对于从Google进入此处的人,Anatoliy的代码有效,但在鼠标滚轮滚动方面存在一些问题。
无修复的滚动(请记住,我试图快速滚动到底部)
有修复的滚动
(自我推销,您可以在这里了解此应用程序是什么)
让我们来看看原因。
#region ScrollViewerPreviewMouseWheel Event Handler
private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
double mouseWheelChange = (double)e.Delta;
ScrollViewer scroller = (ScrollViewer)sender;
double newVOffset = GetVerticalOffset(scroller) - (mouseWheelChange / 3);
if (newVOffset < 0)
{
AnimateScroll(scroller, 0);
}
else if (newVOffset > scroller.ScrollableHeight)
{
AnimateScroll(scroller, scroller.ScrollableHeight);
}
else
{
AnimateScroll(scroller, newVOffset);
}
e.Handled = true;
}
在这段处理程序代码中,您会注意到每次滚动鼠标轮时都会调用它。因此,当你快速滚动它时,动画没有完成的时间,你陷入了尝试从动画中间位置滚动的困境。这导致在尝试更快地滚动时出现抖动缓慢滚动。
另外他们的代码在这里:
private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
DoubleAnimation verticalAnimation = new DoubleAnimation();
verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = ToValue;
verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(verticalAnimation);
Storyboard.SetTarget(verticalAnimation, scrollViewer);
Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(ScrollAnimationBehavior.VerticalOffsetProperty));
storyboard.Begin();
}
有不必要的故事板实现,可以将其删除以使滚动动画可中断,这是我们需要平滑快速滚动的方式。
在他们的代码顶部,我们将添加一个新变量。
public static class ScrollAnimationBehavior
{
public static double intendedLocation = 0;
...
我们需要保存动画的预期位置,这样如果再次调用滚动事件,我们可以在开始下一个动画调用之前立即跳转到该位置。现在有另外一件事情需要改变。当用户使用其中一个keydown事件(Page up或page down)或者手动使用鼠标移动滚动条时,需要更新intendedLocation。因此,我们需要添加另一个事件来处理scrollviewer上的左鼠标按钮抬起,并且当鼠标抬起(放置在预期位置)时,我们可以更改预期位置,以便滚动轮获取更新的位置。
private static void SetEventHandlersForScrollViewer(ScrollViewer scroller)
{
scroller.PreviewMouseWheel += new MouseWheelEventHandler(ScrollViewerPreviewMouseWheel);
scroller.PreviewKeyDown += new KeyEventHandler(ScrollViewerPreviewKeyDown);
scroller.PreviewMouseLeftButtonUp += Scroller_PreviewMouseLeftButtonUp;
}
private static void Scroller_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
intendedLocation = ((ScrollViewer)sender).VerticalOffset;
}
我们仍需要更新页面的上下翻页区域。
private static void ScrollViewerPreviewKeyDown(object sender, KeyEventArgs e)
{
ScrollViewer scroller = (ScrollViewer)sender;
Key keyPressed = e.Key;
double newVerticalPos = GetVerticalOffset(scroller);
bool isKeyHandled = false;
if (keyPressed == Key.Down)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + GetPointsToScroll(scroller)), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.PageDown)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + scroller.ViewportHeight), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.Up)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - GetPointsToScroll(scroller)), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.PageUp)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - scroller.ViewportHeight), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
if (newVerticalPos != GetVerticalOffset(scroller))
{
intendedLocation = newVerticalPos;
AnimateScroll(scroller, newVerticalPos);
}
e.Handled = isKeyHandled;
}
现在我们已经处理了非鼠标滚轮事件来更新预期位置,让我们修复鼠标滚轮事件。
private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
double mouseWheelChange = (double)e.Delta;
ScrollViewer scroller = (ScrollViewer)sender;
double newVOffset = intendedLocation - (mouseWheelChange * 2);
scroller.ScrollToVerticalOffset(intendedLocation);
if (newVOffset < 0)
{
newVOffset = 0;
}
if (newVOffset > scroller.ScrollableHeight)
{
newVOffset = scroller.ScrollableHeight;
}
AnimateScroll(scroller, newVOffset);
intendedLocation = newVOffset;
e.Handled = true;
}
所以更改如下:
将newVOffset从intendedLocation和mouseWheelChange更改为函数。
清理了当newVOffset超出可接受边界时的情况。
我们跳转到了由上一个滚轮事件创建的intendedLocation,它表示想要前往的位置。
如果您想更改滚动的“速度”,只需更改
double newVOffset = intendedLocation - (mouseWheelChange * 2);
你可以将修改器从2倍更改为5倍,以获得更快的速度,或者更改为1倍以获得更慢的速度。
现在处理了所有事件,让我们使动画自行取消,这样就有了一个目的。
private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
scrollViewer.BeginAnimation(VerticalOffsetProperty, null);
DoubleAnimation verticalAnimation = new DoubleAnimation();
verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = ToValue;
verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));
scrollViewer.BeginAnimation(VerticalOffsetProperty, verticalAnimation);
}
所以我们做的是去除故事板,并使任何现有的动画失效,以便我们可以开始全新的动画。
下面是完整的代码(按原样提供),如果您懒得更改它就像我一样,只需复制它即可。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Input;
using ScrollAnimateBehavior.Helpers;
namespace ScrollAnimateBehavior.AttachedBehaviors
{
public static class ScrollAnimationBehavior
{
public static double intendedLocation = 0;
#region Private ScrollViewer for ListBox
private static ScrollViewer _listBoxScroller = new ScrollViewer();
#endregion
#region VerticalOffset Property
public static DependencyProperty VerticalOffsetProperty =
DependencyProperty.RegisterAttached("VerticalOffset",
typeof(double),
typeof(ScrollAnimationBehavior),
new UIPropertyMetadata(0.0, OnVerticalOffsetChanged));
public static void SetVerticalOffset(FrameworkElement target, double value)
{
target.SetValue(VerticalOffsetProperty, value);
}
public static double GetVerticalOffset(FrameworkElement target)
{
return (double)target.GetValue(VerticalOffsetProperty);
}
#endregion
#region TimeDuration Property
public static DependencyProperty TimeDurationProperty =
DependencyProperty.RegisterAttached("TimeDuration",
typeof(TimeSpan),
typeof(ScrollAnimationBehavior),
new PropertyMetadata(new TimeSpan(0, 0, 0, 0, 0)));
public static void SetTimeDuration(FrameworkElement target, TimeSpan value)
{
target.SetValue(TimeDurationProperty, value);
}
public static TimeSpan GetTimeDuration(FrameworkElement target)
{
return (TimeSpan)target.GetValue(TimeDurationProperty);
}
#endregion
#region PointsToScroll Property
public static DependencyProperty PointsToScrollProperty =
DependencyProperty.RegisterAttached("PointsToScroll",
typeof(double),
typeof(ScrollAnimationBehavior),
new PropertyMetadata(0.0));
public static void SetPointsToScroll(FrameworkElement target, double value)
{
target.SetValue(PointsToScrollProperty, value);
}
public static double GetPointsToScroll(FrameworkElement target)
{
return (double)target.GetValue(PointsToScrollProperty);
}
#endregion
#region OnVerticalOffset Changed
private static void OnVerticalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
ScrollViewer scrollViewer = target as ScrollViewer;
if (scrollViewer != null)
{
scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
}
}
#endregion
#region IsEnabled Property
public static DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled",
typeof(bool),
typeof(ScrollAnimationBehavior),
new UIPropertyMetadata(false, OnIsEnabledChanged));
public static void SetIsEnabled(FrameworkElement target, bool value)
{
target.SetValue(IsEnabledProperty, value);
}
public static bool GetIsEnabled(FrameworkElement target)
{
return (bool)target.GetValue(IsEnabledProperty);
}
#endregion
#region OnIsEnabledChanged Changed
private static void OnIsEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var target = sender;
if (target != null && target is ScrollViewer)
{
ScrollViewer scroller = target as ScrollViewer;
scroller.Loaded += new RoutedEventHandler(scrollerLoaded);
}
if (target != null && target is ListBox)
{
ListBox listbox = target as ListBox;
listbox.Loaded += new RoutedEventHandler(listboxLoaded);
}
}
#endregion
#region AnimateScroll Helper
private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
scrollViewer.BeginAnimation(VerticalOffsetProperty, null);
DoubleAnimation verticalAnimation = new DoubleAnimation();
verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = ToValue;
verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));
scrollViewer.BeginAnimation(VerticalOffsetProperty, verticalAnimation);
}
#endregion
#region NormalizeScrollPos Helper
private static double NormalizeScrollPos(ScrollViewer scroll, double scrollChange, Orientation o)
{
double returnValue = scrollChange;
if (scrollChange < 0)
{
returnValue = 0;
}
if (o == Orientation.Vertical && scrollChange > scroll.ScrollableHeight)
{
returnValue = scroll.ScrollableHeight;
}
else if (o == Orientation.Horizontal && scrollChange > scroll.ScrollableWidth)
{
returnValue = scroll.ScrollableWidth;
}
return returnValue;
}
#endregion
#region UpdateScrollPosition Helper
private static void UpdateScrollPosition(object sender)
{
ListBox listbox = sender as ListBox;
if (listbox != null)
{
double scrollTo = 0;
for (int i = 0; i < (listbox.SelectedIndex); i++)
{
ListBoxItem tempItem = listbox.ItemContainerGenerator.ContainerFromItem(listbox.Items[i]) as ListBoxItem;
if (tempItem != null)
{
scrollTo += tempItem.ActualHeight;
}
}
AnimateScroll(_listBoxScroller, scrollTo);
}
}
#endregion
#region SetEventHandlersForScrollViewer Helper
private static void SetEventHandlersForScrollViewer(ScrollViewer scroller)
{
scroller.PreviewMouseWheel += new MouseWheelEventHandler(ScrollViewerPreviewMouseWheel);
scroller.PreviewKeyDown += new KeyEventHandler(ScrollViewerPreviewKeyDown);
scroller.PreviewMouseLeftButtonUp += Scroller_PreviewMouseLeftButtonUp;
}
private static void Scroller_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
intendedLocation = ((ScrollViewer)sender).VerticalOffset;
}
#endregion
#region scrollerLoaded Event Handler
private static void scrollerLoaded(object sender, RoutedEventArgs e)
{
ScrollViewer scroller = sender as ScrollViewer;
SetEventHandlersForScrollViewer(scroller);
}
#endregion
#region listboxLoaded Event Handler
private static void listboxLoaded(object sender, RoutedEventArgs e)
{
ListBox listbox = sender as ListBox;
_listBoxScroller = FindVisualChildHelper.GetFirstChildOfType<ScrollViewer>(listbox);
SetEventHandlersForScrollViewer(_listBoxScroller);
SetTimeDuration(_listBoxScroller, new TimeSpan(0, 0, 0, 0, 200));
SetPointsToScroll(_listBoxScroller, 16.0);
listbox.SelectionChanged += new SelectionChangedEventHandler(ListBoxSelectionChanged);
listbox.Loaded += new RoutedEventHandler(ListBoxLoaded);
listbox.LayoutUpdated += new EventHandler(ListBoxLayoutUpdated);
}
#endregion
#region ScrollViewerPreviewMouseWheel Event Handler
private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
double mouseWheelChange = (double)e.Delta;
ScrollViewer scroller = (ScrollViewer)sender;
double newVOffset = intendedLocation - (mouseWheelChange * 2);
scroller.ScrollToVerticalOffset(intendedLocation);
if (newVOffset < 0)
{
newVOffset = 0;
}
if (newVOffset > scroller.ScrollableHeight)
{
newVOffset = scroller.ScrollableHeight;
}
AnimateScroll(scroller, newVOffset);
intendedLocation = newVOffset;
e.Handled = true;
}
#endregion
#region ScrollViewerPreviewKeyDown Handler
private static void ScrollViewerPreviewKeyDown(object sender, KeyEventArgs e)
{
ScrollViewer scroller = (ScrollViewer)sender;
Key keyPressed = e.Key;
double newVerticalPos = GetVerticalOffset(scroller);
bool isKeyHandled = false;
if (keyPressed == Key.Down)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + GetPointsToScroll(scroller)), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.PageDown)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + scroller.ViewportHeight), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.Up)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - GetPointsToScroll(scroller)), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.PageUp)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - scroller.ViewportHeight), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
if (newVerticalPos != GetVerticalOffset(scroller))
{
intendedLocation = newVerticalPos;
AnimateScroll(scroller, newVerticalPos);
}
e.Handled = isKeyHandled;
}
#endregion
#region ListBox Event Handlers
private static void ListBoxLayoutUpdated(object sender, EventArgs e)
{
UpdateScrollPosition(sender);
}
private static void ListBoxLoaded(object sender, RoutedEventArgs e)
{
UpdateScrollPosition(sender);
}
private static void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
UpdateScrollPosition(sender);
}
#endregion
}
}
ScrollViewer
控件。我意识到这有点棘手...但是,对现有控件进行子类化似乎太有问题了,因为缺乏动画效果已经固定在其中。 - McGarnagleScrollBar
和一个带有隐藏滚动条的ScrollViewer
组成,以将滚动条与滚动视图器分离。然后,我可以轻松地实现摩擦滚动,因为我手动处理了滚动条拖动。 - Ming Slogar