using System.Windows.Media.Animation; using System.Windows.Threading; using NeoUI.Assists; namespace NeoUI.Controls; /// /// 锚点导航控件,提供页面内锚点跳转和高亮功能 /// 支持自动扫描带有 AnchorAssist.Header 属性的元素,并生成导航列表 /// [TemplatePart(Name = "PART_AnchorList", Type = typeof(ListBox))] [TemplatePart(Name = "PART_ContentScroller", Type = typeof(ScrollViewer))] public class Anchor : ContentControl { /// /// 滚动状态枚举 /// private enum ScrollState { /// 空闲状态,可以响应用户交互 Idle, /// 动画滚动中,阻止手动滚动的高亮更新 Animating, /// 手动滚动中 ManualScrolling } #region 私有字段 /// 锚点列表控件 private ListBox? anchorList; /// 内容滚动视图 private ScrollViewer? contentScroller; /// 当前滚动状态 private ScrollState currentState = ScrollState.Idle; /// 扫描防抖计时器,避免频繁扫描锚点 private DispatcherTimer? scanDebounceTimer; /// 锚点名称到UI元素的映射字典 private readonly Dictionary anchorElements = new(); #endregion #region 构造函数和模板应用 static Anchor() { DefaultStyleKeyProperty.OverrideMetadata(typeof(Anchor), new FrameworkPropertyMetadata(typeof(Anchor))); } /// /// 应用控件模板,初始化内部控件引用和事件绑定 /// public override void OnApplyTemplate() { base.OnApplyTemplate(); // 清理旧的事件绑定 UnbindEvents(); // 停止旧的计时器 scanDebounceTimer?.Stop(); // 获取模板中的控件 anchorList = (ListBox)GetTemplateChild("PART_AnchorList"); contentScroller = (ScrollViewer)GetTemplateChild("PART_ContentScroller"); // 绑定新的事件和初始化 if (anchorList != null && contentScroller != null) { BindEvents(); InitializeScanTimer(); // 延迟扫描,确保控件完全加载 contentScroller.Loaded += RequestScan; } } /// /// 解绑事件处理器 /// private void UnbindEvents() { if (anchorList != null) anchorList.SelectionChanged -= AnchorList_SelectionChanged; if (contentScroller != null) { contentScroller.ScrollChanged -= ContentScroller_ScrollChanged; contentScroller.SizeChanged -= RequestScan; if (contentScroller.Content is FrameworkElement fe) fe.LayoutUpdated -= RequestScan; } } /// /// 绑定事件处理器 /// private void BindEvents() { if (anchorList != null) anchorList.SelectionChanged += AnchorList_SelectionChanged; if (contentScroller == null) return; contentScroller.ScrollChanged += ContentScroller_ScrollChanged; contentScroller.SizeChanged += RequestScan; if (contentScroller.Content is FrameworkElement fe2) fe2.LayoutUpdated += RequestScan; } /// /// 初始化扫描防抖计时器 /// private void InitializeScanTimer() { scanDebounceTimer = new DispatcherTimer(DispatcherPriority.ContextIdle, Dispatcher) { Interval = TimeSpan.FromMilliseconds(100) // 150ms 防抖延迟 }; scanDebounceTimer.Tick += (_, _) => { scanDebounceTimer.Stop(); ScanAnchors(); }; } #endregion #region 锚点扫描 /// /// 请求扫描锚点(防抖处理) /// private void RequestScan(object sender, EventArgs e) { scanDebounceTimer?.Stop(); scanDebounceTimer?.Start(); } /// /// 扫描内容区域中的所有锚点元素 /// 查找带有 AnchorAssist.Header 属性的UI元素并更新锚点列表 /// private void ScanAnchors() { if (contentScroller == null || anchorList == null) return; // 清空现有锚点映射 anchorElements.Clear(); // 查找所有带有锚点标识的UI元素 List anchorUiElements = FindVisualChildren(contentScroller) .Where(el => VisualTreeHelper.GetParent(el) != null && !string.IsNullOrEmpty(AnchorAssist.GetHeader(el))) .ToList(); // 构建锚点名称到元素的映射 foreach (var element in anchorUiElements) { anchorElements[AnchorAssist.GetHeader(element)] = element; } // 更新锚点列表数据源(仅在内容变化时) var newNames = anchorElements.Keys.ToList(); var currentNames = anchorList.ItemsSource?.Cast().ToList() ?? new List(); if (!currentNames.SequenceEqual(newNames)) { anchorList.ItemsSource = newNames; } // 更新当前高亮项 UpdateHighlight(); } #endregion #region 事件处理 /// /// 锚点列表选择变化事件处理 /// 执行平滑滚动到选中的锚点位置 /// private void AnchorList_SelectionChanged(object sender, SelectionChangedEventArgs e) { // 仅在空闲状态且有选中项时处理 if (currentState != ScrollState.Idle || e.AddedItems.Count == 0) return; var selectedAnchorName = e.AddedItems[0] as string; if (selectedAnchorName != null && anchorElements.TryGetValue(selectedAnchorName, out var targetElement)) { ScrollToElement(targetElement); } } /// /// 滚动到指定元素位置(带动画效果) /// /// 目标元素 private void ScrollToElement(UIElement targetElement) { try { // 计算目标元素相对于滚动视图的位置 if (contentScroller == null) return; var transform = targetElement.TransformToVisual(contentScroller); var position = transform.Transform(new Point(0, 0)); var targetOffset = contentScroller.VerticalOffset + position.Y; // 使用中介器执行动画滚动 var mediator = new ScrollViewerOffsetMediator { ScrollViewer = contentScroller }; // 取消之前的动画 mediator.BeginAnimation(ScrollViewerOffsetMediator.VerticalOffsetProperty, null); // 创建平滑滚动动画 var animation = new DoubleAnimation( contentScroller.VerticalOffset, targetOffset, TimeSpan.FromMilliseconds(400)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } }; // 设置动画状态和完成回调 currentState = ScrollState.Animating; animation.Completed += (_, _) => { currentState = ScrollState.Idle; UpdateHighlight(); // 动画完成后更新高亮 }; mediator.BeginAnimation(ScrollViewerOffsetMediator.VerticalOffsetProperty, animation); } catch { // 防止 TransformToVisual 在元素未完全渲染时抛出异常 } } /// /// 内容滚动变化事件处理 /// 在手动滚动时更新锚点高亮 /// private void ContentScroller_ScrollChanged(object sender, ScrollChangedEventArgs e) { // 动画滚动期间忽略滚动事件 if (currentState == ScrollState.Animating) return; // 仅处理垂直滚动 if (e.VerticalChange != 0) { currentState = ScrollState.ManualScrolling; UpdateHighlight(); currentState = ScrollState.Idle; } } #endregion #region 高亮更新 /// /// 更新锚点列表中的高亮项 /// 根据当前滚动位置计算最合适的锚点并设为选中状态 /// private void UpdateHighlight() { if (anchorElements.Count == 0 || anchorList == null || contentScroller == null) return; UIElement? bestMatchElement = null; // 规则 #1: 如果滚动到底部,高亮最后一个锚点 if (contentScroller.VerticalOffset >= contentScroller.ScrollableHeight - 1.0 && contentScroller.ScrollableHeight > 0) { bestMatchElement = anchorElements.LastOrDefault().Value; } else { // 规则 #2: 找到最接近视窗顶部且仍然可见的锚点 var bestMatchDistance = double.NegativeInfinity; foreach (var element in anchorElements.Values) { if (element != null && VisualTreeHelper.GetParent(element) == null) continue; try { var transform = element?.TransformToVisual(contentScroller); if (transform != null) { var position = transform.Transform(new Point(0, 0)); // 选择最接近视窗顶部但仍在视窗内的元素 if (position.Y <= 0.5 && position.Y > bestMatchDistance) { bestMatchDistance = position.Y; bestMatchElement = element; } } } catch { // 安全容错处理 } } } // 回退策略:如果没有找到合适的元素,选择第一个 bestMatchElement ??= anchorElements.FirstOrDefault().Value; // 更新列表选中项(避免重复设置) if (bestMatchElement == null) return; var anchorName = AnchorAssist.GetHeader(bestMatchElement); if ((string)anchorList.SelectedItem == anchorName) return; // 临时设置为动画状态,防止触发选择变化事件 currentState = ScrollState.Animating; anchorList.SelectedItem = anchorName; currentState = ScrollState.Idle; } #endregion #region 工具方法 /// /// 递归查找可视化树中指定类型的所有子元素 /// /// 要查找的元素类型 /// 起始依赖对象 /// 找到的所有匹配元素 private static IEnumerable FindVisualChildren(DependencyObject? depObj) where T : DependencyObject { if (depObj == null) yield break; for (var i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) { var child = VisualTreeHelper.GetChild(depObj, i); if (child is T t) yield return t; // 递归查找子元素 foreach (var descendant in FindVisualChildren(child)) yield return descendant; } } #endregion } /// /// ScrollViewer 偏移量动画中介器 /// 由于 ScrollViewer.VerticalOffset 不是依赖属性,无法直接动画化 /// 此类作为中介,将动画值传递给 ScrollViewer.ScrollToVerticalOffset 方法 /// internal class ScrollViewerOffsetMediator : Animatable { #region 依赖属性 /// /// 关联的 ScrollViewer 控件 /// public ScrollViewer? ScrollViewer { get => (ScrollViewer)GetValue(ScrollViewerProperty); set => SetValue(ScrollViewerProperty, value); } public static readonly DependencyProperty ScrollViewerProperty = DependencyProperty.Register( nameof(ScrollViewer), typeof(ScrollViewer), typeof(ScrollViewerOffsetMediator), new PropertyMetadata(null)); /// /// 垂直偏移量(可动画化的依赖属性) /// public double VerticalOffset { get => (double)GetValue(VerticalOffsetProperty); set => SetValue(VerticalOffsetProperty, value); } public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register( nameof(VerticalOffset), typeof(double), typeof(ScrollViewerOffsetMediator), new PropertyMetadata(0.0, OnVerticalOffsetChanged)); #endregion /// /// 垂直偏移量属性变化回调 /// 将动画值同步到 ScrollViewer 的实际滚动位置 /// private static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not ScrollViewerOffsetMediator mediator) return; if (mediator.ScrollViewer?.IsLoaded == true) { // ScrollViewer 已加载,直接滚动 mediator.ScrollViewer.ScrollToVerticalOffset((double)e.NewValue); } else { // ScrollViewer 未完全加载,延迟到加载完成后执行 mediator.ScrollViewer?.Dispatcher.BeginInvoke(() => { mediator.ScrollViewer.ScrollToVerticalOffset((double)e.NewValue); }, System.Windows.Threading.DispatcherPriority.Loaded); } } /// /// 创建 Freezable 实例(动画系统要求) /// protected override Freezable CreateInstanceCore() { return new ScrollViewerOffsetMediator(); } }