整理
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
using NeumUI.Assists;
|
||||
|
||||
namespace NeumUI.Controls;
|
||||
using NeoUI.Assists;
|
||||
|
||||
namespace NeoUI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// 锚点导航控件,提供页面内锚点跳转和高亮功能
|
||||
@@ -28,19 +29,19 @@ public class Anchor : ContentControl
|
||||
#region 私有字段
|
||||
|
||||
/// <summary>锚点列表控件</summary>
|
||||
private ListBox anchorList;
|
||||
|
||||
private ListBox? anchorList;
|
||||
|
||||
/// <summary>内容滚动视图</summary>
|
||||
private ScrollViewer contentScroller;
|
||||
|
||||
private ScrollViewer? contentScroller;
|
||||
|
||||
/// <summary>当前滚动状态</summary>
|
||||
private ScrollState currentState = ScrollState.Idle;
|
||||
|
||||
|
||||
/// <summary>扫描防抖计时器,避免频繁扫描锚点</summary>
|
||||
private DispatcherTimer scanDebounceTimer;
|
||||
private DispatcherTimer? scanDebounceTimer;
|
||||
|
||||
/// <summary>锚点名称到UI元素的映射字典</summary>
|
||||
private readonly Dictionary<string, UIElement> anchorElements = new();
|
||||
private readonly Dictionary<string, UIElement?> anchorElements = new();
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -73,7 +74,7 @@ public class Anchor : ContentControl
|
||||
{
|
||||
BindEvents();
|
||||
InitializeScanTimer();
|
||||
|
||||
|
||||
// 延迟扫描,确保控件完全加载
|
||||
contentScroller.Loaded += RequestScan;
|
||||
|
||||
@@ -85,14 +86,14 @@ public class Anchor : ContentControl
|
||||
/// </summary>
|
||||
private void UnbindEvents()
|
||||
{
|
||||
if (anchorList != null)
|
||||
if (anchorList != null)
|
||||
anchorList.SelectionChanged -= AnchorList_SelectionChanged;
|
||||
|
||||
|
||||
if (contentScroller != null)
|
||||
{
|
||||
contentScroller.ScrollChanged -= ContentScroller_ScrollChanged;
|
||||
contentScroller.SizeChanged -= RequestScan;
|
||||
if (contentScroller.Content is FrameworkElement fe)
|
||||
if (contentScroller.Content is FrameworkElement fe)
|
||||
fe.LayoutUpdated -= RequestScan;
|
||||
}
|
||||
}
|
||||
@@ -102,10 +103,11 @@ public class Anchor : ContentControl
|
||||
/// </summary>
|
||||
private void BindEvents()
|
||||
{
|
||||
anchorList.SelectionChanged += AnchorList_SelectionChanged;
|
||||
if (anchorList != null) anchorList.SelectionChanged += AnchorList_SelectionChanged;
|
||||
if (contentScroller == null) return;
|
||||
contentScroller.ScrollChanged += ContentScroller_ScrollChanged;
|
||||
contentScroller.SizeChanged += RequestScan;
|
||||
if (contentScroller.Content is FrameworkElement fe2)
|
||||
if (contentScroller.Content is FrameworkElement fe2)
|
||||
fe2.LayoutUpdated += RequestScan;
|
||||
}
|
||||
|
||||
@@ -116,7 +118,7 @@ public class Anchor : ContentControl
|
||||
{
|
||||
scanDebounceTimer = new DispatcherTimer(DispatcherPriority.ContextIdle, Dispatcher)
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(150) // 150ms 防抖延迟
|
||||
Interval = TimeSpan.FromMilliseconds(100) // 150ms 防抖延迟
|
||||
};
|
||||
scanDebounceTimer.Tick += (_, _) =>
|
||||
{
|
||||
@@ -150,8 +152,8 @@ public class Anchor : ContentControl
|
||||
anchorElements.Clear();
|
||||
|
||||
// 查找所有带有锚点标识的UI元素
|
||||
var anchorUiElements = FindVisualChildren<UIElement>(contentScroller)
|
||||
.Where(el => VisualTreeHelper.GetParent(el) != null &&
|
||||
List<UIElement?> anchorUiElements = FindVisualChildren<UIElement>(contentScroller)
|
||||
.Where(el => VisualTreeHelper.GetParent(el) != null &&
|
||||
!string.IsNullOrEmpty(AnchorAssist.GetHeader(el)))
|
||||
.ToList();
|
||||
|
||||
@@ -188,7 +190,7 @@ public class Anchor : ContentControl
|
||||
if (currentState != ScrollState.Idle || e.AddedItems.Count == 0) return;
|
||||
|
||||
var selectedAnchorName = e.AddedItems[0] as string;
|
||||
if (anchorElements.TryGetValue(selectedAnchorName, out var targetElement))
|
||||
if (selectedAnchorName != null && anchorElements.TryGetValue(selectedAnchorName, out var targetElement))
|
||||
{
|
||||
ScrollToElement(targetElement);
|
||||
}
|
||||
@@ -203,6 +205,7 @@ public class Anchor : ContentControl
|
||||
try
|
||||
{
|
||||
// 计算目标元素相对于滚动视图的位置
|
||||
if (contentScroller == null) return;
|
||||
var transform = targetElement.TransformToVisual(contentScroller);
|
||||
var position = transform.Transform(new Point(0, 0));
|
||||
var targetOffset = contentScroller.VerticalOffset + position.Y;
|
||||
@@ -215,8 +218,8 @@ public class Anchor : ContentControl
|
||||
|
||||
// 创建平滑滚动动画
|
||||
var animation = new DoubleAnimation(
|
||||
contentScroller.VerticalOffset,
|
||||
targetOffset,
|
||||
contentScroller.VerticalOffset,
|
||||
targetOffset,
|
||||
TimeSpan.FromMilliseconds(400))
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||
@@ -229,7 +232,7 @@ public class Anchor : ContentControl
|
||||
currentState = ScrollState.Idle;
|
||||
UpdateHighlight(); // 动画完成后更新高亮
|
||||
};
|
||||
|
||||
|
||||
mediator.BeginAnimation(ScrollViewerOffsetMediator.VerticalOffsetProperty, animation);
|
||||
}
|
||||
catch
|
||||
@@ -268,10 +271,10 @@ public class Anchor : ContentControl
|
||||
{
|
||||
if (anchorElements.Count == 0 || anchorList == null || contentScroller == null) return;
|
||||
|
||||
UIElement bestMatchElement = null;
|
||||
UIElement? bestMatchElement = null;
|
||||
|
||||
// 规则 #1: 如果滚动到底部,高亮最后一个锚点
|
||||
if (contentScroller.VerticalOffset >= contentScroller.ScrollableHeight - 1.0 &&
|
||||
if (contentScroller.VerticalOffset >= contentScroller.ScrollableHeight - 1.0 &&
|
||||
contentScroller.ScrollableHeight > 0)
|
||||
{
|
||||
bestMatchElement = anchorElements.LastOrDefault().Value;
|
||||
@@ -283,18 +286,21 @@ public class Anchor : ContentControl
|
||||
|
||||
foreach (var element in anchorElements.Values)
|
||||
{
|
||||
if (VisualTreeHelper.GetParent(element) == null) continue;
|
||||
if (element != null && VisualTreeHelper.GetParent(element) == null) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var transform = element.TransformToVisual(contentScroller);
|
||||
var position = transform.Transform(new Point(0, 0));
|
||||
|
||||
// 选择最接近视窗顶部但仍在视窗内的元素
|
||||
if (position.Y <= 0.5 && position.Y > bestMatchDistance)
|
||||
var transform = element?.TransformToVisual(contentScroller);
|
||||
if (transform != null)
|
||||
{
|
||||
bestMatchDistance = position.Y;
|
||||
bestMatchElement = element;
|
||||
var position = transform.Transform(new Point(0, 0));
|
||||
|
||||
// 选择最接近视窗顶部但仍在视窗内的元素
|
||||
if (position.Y <= 0.5 && position.Y > bestMatchDistance)
|
||||
{
|
||||
bestMatchDistance = position.Y;
|
||||
bestMatchElement = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -305,23 +311,16 @@ public class Anchor : ContentControl
|
||||
}
|
||||
|
||||
// 回退策略:如果没有找到合适的元素,选择第一个
|
||||
if (bestMatchElement == null)
|
||||
{
|
||||
bestMatchElement = anchorElements.FirstOrDefault().Value;
|
||||
}
|
||||
bestMatchElement ??= anchorElements.FirstOrDefault().Value;
|
||||
|
||||
// 更新列表选中项(避免重复设置)
|
||||
if (bestMatchElement != null)
|
||||
{
|
||||
var anchorName = AnchorAssist.GetHeader(bestMatchElement);
|
||||
if ((string)anchorList.SelectedItem != anchorName)
|
||||
{
|
||||
// 临时设置为动画状态,防止触发选择变化事件
|
||||
currentState = ScrollState.Animating;
|
||||
anchorList.SelectedItem = anchorName;
|
||||
currentState = ScrollState.Idle;
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -334,7 +333,7 @@ public class Anchor : ContentControl
|
||||
/// <typeparam name="T">要查找的元素类型</typeparam>
|
||||
/// <param name="depObj">起始依赖对象</param>
|
||||
/// <returns>找到的所有匹配元素</returns>
|
||||
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
|
||||
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject? depObj) where T : DependencyObject
|
||||
{
|
||||
if (depObj == null) yield break;
|
||||
|
||||
@@ -365,7 +364,7 @@ internal class ScrollViewerOffsetMediator : Animatable
|
||||
/// <summary>
|
||||
/// 关联的 ScrollViewer 控件
|
||||
/// </summary>
|
||||
public ScrollViewer ScrollViewer
|
||||
public ScrollViewer? ScrollViewer
|
||||
{
|
||||
get => (ScrollViewer)GetValue(ScrollViewerProperty);
|
||||
set => SetValue(ScrollViewerProperty, value);
|
||||
@@ -402,21 +401,19 @@ internal class ScrollViewerOffsetMediator : Animatable
|
||||
/// </summary>
|
||||
private static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollViewerOffsetMediator mediator)
|
||||
if (d is not ScrollViewerOffsetMediator mediator) return;
|
||||
if (mediator.ScrollViewer?.IsLoaded == true)
|
||||
{
|
||||
if (mediator.ScrollViewer?.IsLoaded == true)
|
||||
// ScrollViewer 已加载,直接滚动
|
||||
mediator.ScrollViewer.ScrollToVerticalOffset((double)e.NewValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ScrollViewer 未完全加载,延迟到加载完成后执行
|
||||
mediator.ScrollViewer?.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
// ScrollViewer 已加载,直接滚动
|
||||
mediator.ScrollViewer.ScrollToVerticalOffset((double)e.NewValue);
|
||||
}
|
||||
else if (mediator.ScrollViewer != null)
|
||||
{
|
||||
// ScrollViewer 未完全加载,延迟到加载完成后执行
|
||||
mediator.ScrollViewer.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
mediator.ScrollViewer.ScrollToVerticalOffset((double)e.NewValue);
|
||||
}, System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
}, System.Windows.Threading.DispatcherPriority.Loaded);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user