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();
}
}