This commit is contained in:
ShrlAlgo
2025-08-20 12:10:35 +08:00
parent fcd306b0f7
commit 955a01f564
962 changed files with 7893 additions and 127784 deletions

View File

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