using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media.Animation; using TomsToolbox.Wpf; using TomsToolbox.Wpf.Interactivity; namespace LibScrollingOptimization { /// /// /// public class SmoothScrollingBehavior : FrameworkElementBehavior { private ScrollViewer? _scrollViewer; private ScrollContentPresenter? _scrollContentPresenter; private delegate bool GetBool(ScrollViewer scrollViewer); private static readonly GetBool _propertyHandlesMouseWheelScrollingGetter; private static readonly IEasingFunction _scrollingAnimationEase = new CubicEase() { EasingMode = EasingMode.EaseOut }; private const long _millisecondsBetweenTouchpadScrolling = 100; private bool _animationRunning = false; private int _lastScrollDelta = 0; private int _lastVerticalScrollingDelta = 0; private int _lastHorizontalScrollingDelta = 0; private long _lastScrollingTick; static SmoothScrollingBehavior() { #if NETCOREAPP _propertyHandlesMouseWheelScrollingGetter = typeof(ScrollViewer) .GetProperty("HandlesMouseWheelScrolling", BindingFlags.Instance | BindingFlags.NonPublic)! .GetGetMethod(true)! .CreateDelegate(); #else _propertyHandlesMouseWheelScrollingGetter = (GetBool)typeof(ScrollViewer) .GetProperty("HandlesMouseWheelScrolling", BindingFlags.Instance | BindingFlags.NonPublic)! .GetGetMethod(true)! .CreateDelegate(typeof(GetBool)); #endif } [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_ScrollInfo")] static extern IScrollInfo GetScrollInfo(ScrollViewer scrollViewer); /// protected override void OnAssociatedObjectLoaded() { base.OnAssociatedObjectLoaded(); _scrollViewer = AssociatedObject.VisualDescendantsAndSelf().OfType().FirstOrDefault(); if (_scrollViewer == null) return; _scrollViewer.PreviewMouseWheel += ScrollViewer_PreviewMouseWheel; _scrollContentPresenter = _scrollViewer.VisualDescendants().OfType().FirstOrDefault(); } /// protected override void OnAssociatedObjectUnloaded() { base.OnAssociatedObjectUnloaded(); if (_scrollViewer == null) return; _scrollViewer.PreviewMouseWheel -= ScrollViewer_PreviewMouseWheel; } private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e) { if (!ScrollWithWheelDelta) { return; } else { Debug.WriteLine(e.Delta); CoreScrollWithWheelDelta(e); } } private void CoreScrollWithWheelDelta(MouseWheelEventArgs e) { if (_scrollViewer == null || _scrollContentPresenter == null) return; if (e.Handled) { return; } if (!AlwaysHandleMouseWheelScrolling && !_propertyHandlesMouseWheelScrollingGetter.Invoke(_scrollViewer)) { return; } bool vertical = _scrollViewer.ExtentHeight > 0; bool horizontal = _scrollViewer.ExtentWidth > 0; var tickCount = Environment.TickCount; var isTouchpadScrolling = e.Delta % Mouse.MouseWheelDeltaForOneLine != 0 || (tickCount - _lastScrollingTick < _millisecondsBetweenTouchpadScrolling && _lastScrollDelta % Mouse.MouseWheelDeltaForOneLine != 0); double scrollDelta = e.Delta; if (isTouchpadScrolling) { // touchpad 应该滚动更慢一些, 所以这里预先除以一个合适的值 scrollDelta /= 2; // scrollDelta *= TouchpadScrollDeltaFactor; } else { scrollDelta *= MouseScrollDeltaFactor; } if (vertical) { if (GetScrollInfo(_scrollViewer) is IScrollInfo scrollInfo) { // 考虑到 VirtualizingPanel 可能是虚拟的大小, 所以这里需要校正 Delta scrollDelta *= scrollInfo.ViewportHeight / (_scrollContentPresenter?.ActualHeight ?? _scrollViewer.ActualHeight); } var sameDirectionAsLast = Math.Sign(e.Delta) == Math.Sign(_lastVerticalScrollingDelta); var nowOffset = sameDirectionAsLast && _animationRunning ? VerticalOffsetTarget : _scrollViewer.VerticalOffset; var newOffset = nowOffset - scrollDelta; if (newOffset < 0) newOffset = 0; if (newOffset > _scrollViewer.ScrollableHeight) newOffset = _scrollViewer.ScrollableHeight; SetValue(VerticalOffsetTargetPropertyKey, newOffset); _scrollViewer.BeginAnimation(ScrollViewerUtils.VerticalOffsetProperty, null); if (!EnableScrollingAnimation || isTouchpadScrolling) { _scrollViewer.ScrollToVerticalOffset(newOffset); } else { var diff = newOffset - _scrollViewer.VerticalOffset; var absDiff = Math.Abs(diff); var duration = ScrollingAnimationDuration; if (absDiff < Mouse.MouseWheelDeltaForOneLine) { duration = new Duration(TimeSpan.FromTicks((long)(duration.TimeSpan.Ticks * absDiff / Mouse.MouseWheelDeltaForOneLine))); } DoubleAnimation doubleAnimation = new DoubleAnimation() { EasingFunction = _scrollingAnimationEase, Duration = duration, From = _scrollViewer.VerticalOffset, To = newOffset, }; doubleAnimation.Completed += DoubleAnimation_Completed; _animationRunning = true; _scrollViewer.BeginAnimation(ScrollViewerUtils.VerticalOffsetProperty, doubleAnimation, HandoffBehavior.SnapshotAndReplace); } _lastVerticalScrollingDelta = e.Delta; } else if (horizontal) { if (GetScrollInfo(_scrollViewer) is IScrollInfo scrollInfo) { // 考虑到 VirtualizingPanel 可能是虚拟的大小, 所以这里需要校正 Delta scrollDelta *= scrollInfo.ViewportWidth / (_scrollContentPresenter?.ActualWidth ?? _scrollViewer.ActualWidth); } var sameDirectionAsLast = Math.Sign(e.Delta) == Math.Sign(_lastHorizontalScrollingDelta); var nowOffset = sameDirectionAsLast && _animationRunning ? HorizontalOffsetTarget : _scrollViewer.HorizontalOffset; var newOffset = nowOffset - scrollDelta; if (newOffset < 0) newOffset = 0; if (newOffset > _scrollViewer.ScrollableWidth) newOffset = _scrollViewer.ScrollableWidth; SetValue(HorizontalOffsetTargetPropertyKey, newOffset); _scrollViewer.BeginAnimation(ScrollViewerUtils.HorizontalOffsetProperty, null); if (!EnableScrollingAnimation || isTouchpadScrolling) { _scrollViewer.ScrollToHorizontalOffset(newOffset); } else { var diff = newOffset - _scrollViewer.HorizontalOffset; var absDiff = Math.Abs(diff); var duration = ScrollingAnimationDuration; if (absDiff < Mouse.MouseWheelDeltaForOneLine) { duration = new Duration(TimeSpan.FromTicks((long)(duration.TimeSpan.Ticks * absDiff / Mouse.MouseWheelDeltaForOneLine))); } DoubleAnimation doubleAnimation = new DoubleAnimation() { EasingFunction = _scrollingAnimationEase, Duration = duration, From = _scrollViewer.HorizontalOffset, To = newOffset, }; doubleAnimation.Completed += DoubleAnimation_Completed; _animationRunning = true; _scrollViewer.BeginAnimation(ScrollViewerUtils.HorizontalOffsetProperty, doubleAnimation, HandoffBehavior.SnapshotAndReplace); } _lastHorizontalScrollingDelta = e.Delta; } _lastScrollingTick = tickCount; _lastScrollDelta = e.Delta; e.Handled = true; } private void DoubleAnimation_Completed(object? sender, EventArgs e) { _animationRunning = false; } /// /// The horizontal offset of scrolling target /// public double HorizontalOffsetTarget { get { return (double)GetValue(HorizontalOffsetTargetProperty); } } /// /// The vertical offset of scrolling target /// public double VerticalOffsetTarget { get { return (double)GetValue(VerticalOffsetTargetProperty); } } /// /// Scroll with wheel delta instead of scrolling fixed number of lines /// public bool ScrollWithWheelDelta { get { return (bool)GetValue(ScrollWithWheelDeltaProperty); } set { SetValue(ScrollWithWheelDeltaProperty, value); } } /// /// Enable scrolling animation while using mouse
/// You need to set ScrollWithWheelDelta to true to use this ///
public bool EnableScrollingAnimation { get { return (bool)GetValue(EnableScrollingAnimationProperty); } set { SetValue(EnableScrollingAnimationProperty, value); } } /// /// Scrolling animation duration /// public Duration ScrollingAnimationDuration { get { return (Duration)GetValue(ScrollingAnimationDurationProperty); } set { SetValue(ScrollingAnimationDurationProperty, value); } } /// /// Delta value factor while mouse scrolling /// public double MouseScrollDeltaFactor { get { return (double)GetValue(MouseScrollDeltaFactorProperty); } set { SetValue(MouseScrollDeltaFactorProperty, value); } } /// /// Delta value factor while touchpad scrolling /// public double TouchpadScrollDeltaFactor { get { return (double)GetValue(TouchpadScrollDeltaFactorProperty); } set { SetValue(TouchpadScrollDeltaFactorProperty, value); } } /// /// Always handle mouse wheel scrolling.
/// (Especially in "TextBox") ///
public bool AlwaysHandleMouseWheelScrolling { get { return (bool)GetValue(AlwaysHandleMouseWheelScrollingProperty); } set { SetValue(AlwaysHandleMouseWheelScrollingProperty, value); } } /// /// The key needed set a read-only property /// public static readonly DependencyPropertyKey HorizontalOffsetTargetPropertyKey = DependencyProperty.RegisterReadOnly(nameof(HorizontalOffsetTarget), typeof(double), typeof(SmoothScrollingBehavior), new PropertyMetadata(0.0)); /// /// The key needed set a read-only property /// public static readonly DependencyPropertyKey VerticalOffsetTargetPropertyKey = DependencyProperty.RegisterReadOnly(nameof(VerticalOffsetTarget), typeof(double), typeof(SmoothScrollingBehavior), new PropertyMetadata(0.0)); /// /// The key needed set a read-only property /// public static readonly DependencyProperty HorizontalOffsetTargetProperty = HorizontalOffsetTargetPropertyKey.DependencyProperty; /// /// The key needed set a read-only property /// public static readonly DependencyProperty VerticalOffsetTargetProperty = VerticalOffsetTargetPropertyKey.DependencyProperty; /// /// Get value of ScrollWithWheelDelta property /// /// /// public static bool GetScrollWithWheelDelta(DependencyObject obj) { return (bool)obj.GetValue(ScrollWithWheelDeltaProperty); } /// /// Set value of ScrollWithWheelDelta property /// /// /// public static void SetScrollWithWheelDelta(DependencyObject obj, bool value) { obj.SetValue(ScrollWithWheelDeltaProperty, value); } /// /// Get value of EnableScrollingAnimation property /// /// /// public static bool GetEnableScrollingAnimation(DependencyObject obj) { return (bool)obj.GetValue(EnableScrollingAnimationProperty); } /// /// Set value of EnableScrollingAnimation property /// /// /// public static void SetEnableScrollingAnimation(DependencyObject obj, bool value) { obj.SetValue(EnableScrollingAnimationProperty, value); } /// /// Get value of ScrollingAnimationDuration property /// /// /// public static Duration GetScrollingAnimationDuration(DependencyObject obj) { return (Duration)obj.GetValue(ScrollingAnimationDurationProperty); } /// /// Set value of ScrollingAnimationDuration property /// /// /// public static void SetScrollingAnimationDuration(DependencyObject obj, Duration value) { obj.SetValue(ScrollingAnimationDurationProperty, value); } /// /// Set value of AlwaysHandleMouseWheelScrolling property /// /// /// public static bool GetAlwaysHandleMouseWheelScrolling(DependencyObject obj) { return (bool)obj.GetValue(AlwaysHandleMouseWheelScrollingProperty); } /// /// Get value of AlwaysHandleMouseWheelScrolling property /// /// /// public static void SetAlwaysHandleMouseWheelScrolling(DependencyObject obj, bool value) { obj.SetValue(AlwaysHandleMouseWheelScrollingProperty, value); } /// /// The DependencyProperty of property. /// public static readonly DependencyProperty ScrollWithWheelDeltaProperty = DependencyProperty.RegisterAttached(nameof(ScrollWithWheelDelta), typeof(bool), typeof(SmoothScrollingBehavior), new FrameworkPropertyMetadata(true)); /// /// The DependencyProperty of property. /// public static readonly DependencyProperty EnableScrollingAnimationProperty = DependencyProperty.RegisterAttached(nameof(EnableScrollingAnimation), typeof(bool), typeof(SmoothScrollingBehavior), new FrameworkPropertyMetadata(true)); /// /// The DependencyProperty of property. /// public static readonly DependencyProperty ScrollingAnimationDurationProperty = DependencyProperty.RegisterAttached(nameof(ScrollingAnimationDuration), typeof(Duration), typeof(SmoothScrollingBehavior), new FrameworkPropertyMetadata(new Duration(TimeSpan.FromMilliseconds(250))), ValidateScrollingAnimationDuration); /// /// The DependencyProperty of property /// public static readonly DependencyProperty AlwaysHandleMouseWheelScrollingProperty = DependencyProperty.RegisterAttached(nameof(AlwaysHandleMouseWheelScrolling), typeof(bool), typeof(SmoothScrollingBehavior), new FrameworkPropertyMetadata(true)); /// /// The DependencyProperty of property /// public static readonly DependencyProperty MouseScrollDeltaFactorProperty = DependencyProperty.Register(nameof(MouseScrollDeltaFactor), typeof(double), typeof(SmoothScrollingBehavior), new PropertyMetadata(1.0)); /// /// The DependencyProperty of property /// public static readonly DependencyProperty TouchpadScrollDeltaFactorProperty = DependencyProperty.Register(nameof(TouchpadScrollDeltaFactor), typeof(double), typeof(SmoothScrollingBehavior), new PropertyMetadata(1.0)); private static bool ValidateScrollingAnimationDuration(object value) => value is Duration duration && duration.HasTimeSpan; } }