wpf ScrollViewer 上的动画(平滑)滚动

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20731402/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-09 00:02:10  来源:igfitidea点击:

Animated (Smooth) scrolling on ScrollViewer

wpfanimationscrollviewer

提问by Ron

I have a ScrollViewerin my WPF App, and I want it to have smooth/animated scrolling effect just like Firefoxhas (if you know what I am talking about).

ScrollViewer我的 WPF 应用程序中有一个,我希望它像Firefox一样具有平滑/动画滚动效果(如果你知道我在说什么)。

I tried to search over the internet, and the only thing I've found is this:

我试图通过互联网搜索,我发现的唯一一件事是:

How To Create An Animated ScrollViewer (or ListBox) in WPF

如何在 WPF 中创建动画滚动查看器(或列表框)

It works pretty good, but I have one problem with it - it animates the scrolling effect but the ScrollViewer's Thumbgoes directly to the point pressed - I want it to be animated aswell

它工作得很好,但我有一个问题 - 它为滚动效果设置动画,但ScrollViewer'sThumb直接指向按下的点 - 我希望它也被动画化

How can I cause the ScrollViewer's Thumbto be animated as well, or else is there a working control with the same properties/features I want?

我怎样才能使ScrollViewer'sThumb也被动画化,或者是否有一个具有我想要的相同属性/功能的工作控件?

回答by Anatoliy Nikolaev

In your example there are two controls inherited from ScrollViewerand ListBox, the animation is implemented by SplineDoubleKeyFrame[MSDN]. In my time, I realized animation scrolling via the attached dependency property VerticalOffsetProperty, which allows you to directly transfer offset scrollbar into a double animation, like this:

在您的示例中,有两个控件继承自ScrollViewerListBox,动画由SplineDoubleKeyFrame[MSDN] 实现。在我的时代,我通过附加的依赖属性实现了动画滚动VerticalOffsetProperty,它可以让你直接将偏移滚动条转换成双动画,如下所示:

DoubleAnimation verticalAnimation = new DoubleAnimation();

verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = some value;
verticalAnimation.Duration = new Duration( some duration );

Storyboard storyboard = new Storyboard();

storyboard.Children.Add(verticalAnimation);
Storyboard.SetTarget(verticalAnimation, scrollViewer);
Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(ScrollAnimationBehavior.VerticalOffsetProperty)); // Attached dependency property
storyboard.Begin();

Examples can be found here:

示例可以在这里找到:

How to: Animate the Horizontal/VerticalOffset properties of a ScrollViewer

如何:为 ScrollViewer 的 Horizo​​ntal/VerticalOffset 属性设置动画

WPF - Animate ListBox.ScrollViewer.HorizontalOffset?

WPF - 动画 ListBox.ScrollViewer.Horizo​​ntalOffset?

In this case, works well smooth scrolling of the content and the Thumb. Based on this approach, and using your example[How To Create An Animated ScrollViewer (or ListBox) in WPF], I created an attached behavior ScrollAnimationBehavior, which can be applied to ScrollViewerand ListBox.

在这种情况下,可以很好地平滑滚动内容和Thumb. 基于这种方法,并使用您的示例[How To Create An Animated ScrollViewer (or ListBox) in WPF],我创建了一个附加的行为ScrollAnimationBehavior,它可以应用于ScrollViewerListBox

Example of using:

使用示例:

XAML

XAML

<Window x:Class="ScrollAnimateBehavior.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:AttachedBehavior="clr-namespace:ScrollAnimateBehavior.AttachedBehaviors"
        Title="MainWindow" 
        WindowStartupLocation="CenterScreen"
        Height="350"
        Width="525">

    <Window.Resources>
        <x:Array x:Key="TestArray" Type="{x:Type sys:String}">
            <sys:String>TEST 1</sys:String>
            <sys:String>TEST 2</sys:String>
            <sys:String>TEST 3</sys:String>
            <sys:String>TEST 4</sys:String>
            <sys:String>TEST 5</sys:String>
            <sys:String>TEST 6</sys:String>
            <sys:String>TEST 7</sys:String>
            <sys:String>TEST 8</sys:String>
            <sys:String>TEST 9</sys:String>
            <sys:String>TEST 10</sys:String>
        </x:Array>
    </Window.Resources>

    <Grid>
        <TextBlock Text="ScrollViewer"
                   FontFamily="Verdana"
                   FontSize="14"
                   VerticalAlignment="Top"
                   HorizontalAlignment="Left"
                   Margin="80,80,0,0" />

        <ScrollViewer AttachedBehavior:ScrollAnimationBehavior.IsEnabled="True"                         
                      AttachedBehavior:ScrollAnimationBehavior.TimeDuration="00:00:00.20"
                      AttachedBehavior:ScrollAnimationBehavior.PointsToScroll="16"
                      HorizontalAlignment="Left"
                      Width="250"
                      Height="100">

            <StackPanel>
                <ItemsControl ItemsSource="{StaticResource TestArray}"
                              FontSize="16" />
            </StackPanel>
        </ScrollViewer>

        <TextBlock Text="ListBox"
                   FontFamily="Verdana"
                   FontSize="14"
                   VerticalAlignment="Top"
                   HorizontalAlignment="Right"
                   Margin="0,80,100,0" />

        <ListBox AttachedBehavior:ScrollAnimationBehavior.IsEnabled="True"
                 ItemsSource="{StaticResource TestArray}"
                 ScrollViewer.CanContentScroll="False"
                 HorizontalAlignment="Right"
                 FontSize="16"
                 Width="250"
                 Height="100" />        
    </Grid>
</Window>

Output

Output

enter image description here

在此处输入图片说明

IsEnabledproperty is responsible for the scrolling animation for ScrollViewerand for ListBox. Below its implementation:

IsEnabled物业负责滚动动画ScrollViewerListBox。下面是它的实现:

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

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

In these Loadedhandlers are set event handlers for PreviewMouseWheeland PreviewKeyDown.

在这些Loaded处理程序中,为PreviewMouseWheel和设置了事件处理程序PreviewKeyDown

Helpers (auxiliary procedures) are taken from the example and provide a value of doubletype, which is passed to the procedure AnimateScroll(). Here and are the magic key of animation:

辅助程序(辅助程序)取自示例并提供一个doubletype值,该值传递给程序AnimateScroll()。这是动画的神奇钥匙:

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

Some notes

Some notes

  • The example only implemented vertical animation, if you will accept this project, you will realize itself without problems horizontal animation.

  • Selection of the current item in ListBoxnot transferred to the next element of this is due to the interception of events PreviewKeyDown, so you have to think about this moment.

  • This implementation is fully suited for the MVVM pattern. To use this behavior in the Blend, you need to inherit interface Behavior. Example can be found hereand here.

  • 该示例仅实现了垂直动画,如果您接受这个项目,您将毫无问题地实现水平动画。

  • 选择当前 item 中ListBox没有转移到下一个元素 this 是由于事件的拦截PreviewKeyDown,所以你必须考虑这一刻。

  • 此实现完全适用于 MVVM 模式。要在 中使用此行为Blend,您需要继承接口Behavior。可以在此处此处找到示例。

Tested on Windows XP, Windows Seven, .NET 4.0.

Tested on Windows XP, Windows Seven, .NET 4.0.



Sample project is available at this link.

链接提供示例项目。



Below is a full code of this implementation:

下面是这个实现的完整代码:

public static class ScrollAnimationBehavior
{
    #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)
    {
        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();
    }

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

    #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 = 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;
    }

    #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);
            isKeyHandled = true;
        }
        else if (keyPressed == Key.PageDown)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + scroller.ViewportHeight), Orientation.Vertical);
            isKeyHandled = true;
        }
        else if (keyPressed == Key.Up)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - GetPointsToScroll(scroller)), Orientation.Vertical);
            isKeyHandled = true;
        }
        else if (keyPressed == Key.PageUp)
        {
            newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - scroller.ViewportHeight), Orientation.Vertical);
            isKeyHandled = true;
        }

        if (newVerticalPos != GetVerticalOffset(scroller))
        {
            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
}

回答by Mr Arca9

For those getting here from google Anatoliy's code works, but has some issues with mouse wheel scrolling specifically.

对于那些从谷歌 Anatoliy 的代码到达这里的人,但在鼠标滚轮滚动方面存在一些问题。

Scrolling without Fix(Keep in mind i am attempting to rapid scroll to the bottom)

滚动而不修复(请记住,我正在尝试快速滚动到底部)

Scrolling with Fix

滚动修复

(self plug, you can find out what this application is over Here)

(自插,你可以在这里找到这个应用程序是什么)

Let's take a look at why.

让我们来看看为什么。

#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;
}

In this handler code, you'll notice that it is called every time you scroll the mouse wheel. So when you hit it with rapid scrolling the animation doesn't have time to complete, and you are stuck attempting to scroll from where you are in the middle of the animation. This causes jittery slow scrolling when attempting to scroll faster.

在此处理程序代码中,您会注意到每次滚动鼠标滚轮时都会调用它。因此,当您通过快速滚动点击它时,动画没有时间完成,并且您被困在试图从动画中间的位置滚动。当尝试更快地滚动时,这会导致抖动缓慢的滚动。

Additionally their code here:

另外他们的代码在这里:

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

Has unneeded storyboard implementation, that can be removed to make the scroll animation interruptible, which is what we will need to in order to smooth out rapid scrolling.

具有不需要的故事板实现,可以将其删除以使滚动动画可中断,这是我们为了平滑快速滚动所需要的。

At the top of their code we are going to add a new variable.

在他们的代码顶部,我们将添加一个新变量。

public static class ScrollAnimationBehavior
{
    public static double intendedLocation = 0;
...

We need to save the intended location of the animation, so that if we call the scroller event again, we can jump to that location right away before starting the next animation call.

我们需要保存动画的预期位置,这样如果我们再次调用 scroller 事件,我们可以在开始下一次动画调用之前立即跳转到该位置。

Now there's something else we have to change. The intendedLocation needs to be updated when the user either uses one of the keydown events (Page up or page down) or if they manually move the scrollbar with the mouse.

现在还有一些事情我们必须改变。当用户使用 keydown 事件之一(Page up 或 page down)或他们用鼠标手动移动滚动条时,需要更新 expectedLocation。

So we need to add another event to handle the left mouse button up on the scrollviewer, and when the mouse is lifted (put at intended location) then we can change the intended location, so that the scroll wheel gets the updated position.

所以我们需要添加另一个事件来处理滚动查看器上的鼠标左键,当鼠标被抬起(放在预期位置)时,我们可以更改预期位置,以便滚轮获得更新的位置。

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

We still need to update the page up and page down area as well though.

不过,我们仍然需要更新向上翻页和向下翻页区域。

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

Now that we have the non-mousewheel events handled to update the intended location, let's fix up the mouse wheel event.

现在我们已经处理了非鼠标滚轮事件以更新预期位置,让我们修复鼠标滚轮事件。

private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        double mouseWheelChange = (double)e.Delta;
        ScrollViewer scroller = (ScrollViewer)sender;
        double newVOffset = intendedLocation - (mouseWheelChange * 2);
        //Incase we got hit by the mouse again. jump to the offset.
        scroller.ScrollToVerticalOffset(intendedLocation);
        if (newVOffset < 0)
        {
            newVOffset = 0;
        }
        if (newVOffset > scroller.ScrollableHeight)
        {
            newVOffset = scroller.ScrollableHeight;
        }

        AnimateScroll(scroller, newVOffset);
        intendedLocation = newVOffset;
        e.Handled = true;
}

So the changes are as follows

所以变化如下

  1. Changed newVOffset to function from the intendedLocation and the mouseWheelChange.

  2. Cleaned up when the newVOffset is over or under acceptable boundaries.

  3. We jumped to the intendedLocation, which was created by the last scrollwheel event with where it wanted to go.

  1. 将 newVOffset 更改为来自预期位置和 mouseWheelChange 的功能。

  2. 当 newVOffset 超过或低于可接受的边界时进行清理。

  3. 我们跳到了 expectedLocation,它是由最后一个 scrollwheel 事件创建的,带有它想去的地方。

If you want to change the "speed" of the scroll simply change

如果你想改变滚动的“速度”,只需改变

double newVOffset = intendedLocation - (mouseWheelChange * 2);

You can change the modifier from times 2 to times 5 for faster or times 1 for slower.

您可以将修改器从 2 次更改为 5 次以加快速度或将 1 次更改为慢速。

With all the events handled now, let's make the animation cancel itself, so this all has a point.

现在处理完所有事件,让我们让动画自行取消,所以这一切都有道理。

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

So what we did here was remove the storyboard, and nullified any existing animation so that we can start with a fresh new one.

所以我们在这里做的是移除故事板,并取消所有现有的动画,以便我们可以从一个新的开始。

Below is the full code (Provided AS IS), incase you're too lazy to change it like I was just copying it.

下面是完整的代码(按原样提供),以防你懒得像我只是复制它一样更改它。

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);
            //We got hit by the mouse again. jump to the offset.
            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
    }
}

回答by Jamie Clayton

The best example of scroll customization can be found in an article by Sacha Barberon Code Project. See this code project article on Friction scrolling article on the topic.

Sacha Barber在 Code Project 上的一篇文章中可以找到滚动自定义的最佳示例。请参阅有关该主题的摩擦滚动文章的代码项目文章

A number of Sacha Barbers WPF code has been integrated into a Github project for WPF. See MahaApps Metrofor some very useful open source WPF implementations.

许多 Sacha Barbers WPF 代码已集成到 WPF 的 Github 项目中。有关一些非常有用的开源 WPF 实现,请参阅MahaApps Metro