ScrollViewer上的动画(平滑)滚动

38

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

我尝试在互联网上搜索,唯一找到的是这个:

如何在WPF中创建一个带有动画的ScrollViewer(或ListBox)

它的工作非常好,但我有一个问题 - 它会动画地滚动,但是ScrollViewerThumb会直接跳转到按下的位置 - 我想它也要动画效果

如何使ScrollViewerThumb也具有动画效果,或者是否有一个具有我想要的相同属性/特性的工作控件?


唉...链接中的整个方法都非常粗糙:它复制控件和DP以维护现有的控件行为,同时添加动画效果。我尝试了几种方法来获得所需的行为(在“animateScroller”中动画滚动条,使“PART_AniVerticalScrollBar”成为双向绑定),但每种方式都遇到了奇怪的行为。 - McGarnagle
1
我的最佳建议是从头开始重新编写整个ScrollViewer控件。我意识到这有点棘手...但是,对现有控件进行子类化似乎太有问题了,因为缺乏动画效果已经固定在其中。 - McGarnagle
我创建了自己的控件,由两个 ScrollBar 和一个带有隐藏滚动条的 ScrollViewer 组成,以将滚动条与滚动视图器分离。然后,我可以轻松地实现摩擦滚动,因为我手动处理了滚动条拖动。 - Ming Slogar
4个回答

61

在你的示例中,有两个控件继承自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();

示例可以在此处找到:

如何:动画化 ScrollViewer 的 Horizontal/VerticalOffset 属性

WPF - 如何动画化 ListBox.ScrollViewer.HorizontalOffset?

在这种情况下,内容和 Thumb 的平滑滚动效果非常好。基于这种方法,并使用你的示例 [如何在 WPF 中创建一个具有动画效果的 ScrollViewer(或 ListBox)],我创建了一个附加行为 ScrollAnimationBehavior,可以应用于 ScrollViewerListBox

使用示例:

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>

输出

enter image description here

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

这些Loaded处理程序为PreviewMouseWheelPreviewKeyDown设置了事件处理程序。

助手(辅助程序)从示例中获取double类型的值,并将其传递给过程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();
}

一些注意事项

  • 这个示例仅实现了垂直动画,如果您接受了这个项目,那么您将很容易地意识到水平动画。

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

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

已在Windows XP、Windows Seven和.NET 4.0上测试。


示例项目可在此链接中获取。


以下是此实现的完整代码:

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
}

4

对于从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);
        //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;
}

所以更改如下:
  1. 将newVOffset从intendedLocation和mouseWheelChange更改为函数。

  2. 清理了当newVOffset超出可接受边界时的情况。

  3. 我们跳转到了由上一个滚轮事件创建的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);
            //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
    }
}

0

在一些依赖问题上挣扎后,我决定使用滑块硬编码来克服只读。对于感兴趣的人,这是我写的内容。 我还实现了鼠标速度,因此您滚动得越快,滚动就会延伸得更远。如果有人能够将其创建为用户控件,我会很高兴。

<Window x:Class="scrolltest4.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800" Loaded="Window_Loaded">

<Grid>

    <TextBox x:Name="tb3" HorizontalAlignment="Left" Margin="100,292,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="120" />
    <Slider x:Name="SliderH" HorizontalAlignment="Left" ValueChanged="Slider1_ValueChanged" Margin="282,335,0,0" VerticalAlignment="Top" Width="265" Height="21" />
    <Slider x:Name="SliderV" HorizontalAlignment="Left" Margin="753,95,0,0" Orientation="Vertical" IsDirectionReversed="True" VerticalAlignment="Top" Width="18" Height="206" FlowDirection="LeftToRight" ValueChanged="SliderV_ValueChanged" />
    <ScrollViewer x:Name="ScrollViewer1" ScrollChanged="ScrollViewer1_ScrollChanged" Width="auto" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" Margin="129,67,100,217"
                  PreviewMouseLeftButtonDown="ScrollViewer_PreviewMouseLeftButtonDown" PreviewMouseMove="ScrollViewer_PreviewMouseMove" PreviewMouseLeftButtonUp="ScrollViewer_PreviewMouseLeftButtonUp" Background="#FFFFBABA" HorizontalContentAlignment="Center" BorderThickness="2,2,2,2">
        <ScrollViewer.OpacityMask>
            <RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" SpreadMethod="Pad">
                <RadialGradientBrush.RelativeTransform>
                    <TransformGroup>
                        <ScaleTransform CenterY="0.5" CenterX="0.5" ScaleY="1" ScaleX="1" />
                    </TransformGroup>
                </RadialGradientBrush.RelativeTransform>
                <GradientStop Color="Black" Offset="0.532" />
                <GradientStop Offset="1" />
                <GradientStop Color="#FF161616" Offset="0.761" />
            </RadialGradientBrush>
        </ScrollViewer.OpacityMask>
        <StackPanel x:Name="sp1" Orientation="Horizontal" />
    </ScrollViewer>
</Grid>

    using System;
    using System.Collections;
    using System.Diagnostics;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media.Animation;

   namespace scrolltest4
   {

public partial class MainWindow : Window
{
    public System.Windows.Point ScrollMousePoint1 = new System.Windows.Point();
    private Queue TimePoints;
    private double HorizontalOff1 = 1;
    private double Verticaloff1 = 1;
    private double MouseSpeed = 0.1;
    private Stopwatch SW = new Stopwatch();
    private double scrollspeed = 2;

    private double EndV;
    private double EndH;
    private double EndpointH;
    private double EndpointV;

    public MainWindow()
    {
        TimePoints = new Queue(100);
        InitializeComponent();
        Buildstackpanel();
    }

    private void Buildstackpanel()
    {
        int i;

        for (i = 0; i < 80; i++)
        {
            Button button = new Button()
            {
                Content = "b" + i,
                Height = 60,
                Width = 60,
                Margin = new Thickness(5),
                FontSize = 10,
            }; sp1.Children.Add(button);
        }
    }

    private void ScrollViewer_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        SW.Reset();
        SW.Start();
        ScrollMousePoint1 = e.GetPosition(ScrollViewer1);
        HorizontalOff1 = ScrollViewer1.HorizontalOffset;
        Verticaloff1 = ScrollViewer1.HorizontalOffset;
        ScrollViewer1.CaptureMouse();
    }

    private void ScrollViewer_PreviewMouseMove(object sender, MouseEventArgs e)
    {
        if (ScrollViewer1.IsMouseCaptured)
        {
            AddPoint();
            ScrollViewer1.ScrollToHorizontalOffset(HorizontalOff1 + (ScrollMousePoint1.X - e.GetPosition(ScrollViewer1).X));
            ScrollViewer1.ScrollToVerticalOffset(Verticaloff1 + (ScrollMousePoint1.Y - e.GetPosition(ScrollViewer1).Y));
        }
    }

    private void ScrollViewer_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        TimePoints.Clear();
        ScrollViewer1.ReleaseMouseCapture();
        EndpointH = ScrollViewer1.HorizontalOffset;
        EndpointV = ScrollViewer1.VerticalOffset;
        SW.Stop();
        Animate();
    }

    private void AddPoint()
    {
        TimePoints.Enqueue(new TimePoint(Mouse.GetPosition(ScrollViewer1), SW.ElapsedMilliseconds));
        if (TimePoints.Count == 100) TimePoints.Dequeue();
        object[] array = TimePoints.ToArray();
        TimePoint tip = (TimePoint)array[array.Length - 1];
        TimePoint end = (TimePoint)array[0];
        double deltaX = (tip.point.X - end.point.X);
        double deltaY = (tip.point.Y - end.point.Y);
        double distance = deltaX * deltaX + deltaY * deltaY;
        long deltaT = tip.time - end.time;
        MouseSpeed = Math.Sqrt(distance) / SW.ElapsedMilliseconds;
        double velocity_X = deltaX / (double)deltaT;
        double velocity_Y = deltaY / (double)deltaT;
        tb3.Text = string.Format("|V| = {0}; Vx = {1}; Vy = {2}", MouseSpeed, velocity_X, velocity_Y);
    }

    public class TimePoint
    {
        public Point point;
        public long time;

        public TimePoint(Point pt, long ms)
        {
            point = pt;
            time = ms;
        }
    }

    private void ScrollViewer1_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (ScrollViewer1.HorizontalOffset > ScrollViewer1.ScrollableWidth * 0.95)
        {
            for (int i = 0; i < 3; i++)
            {
                Button button = new Button()
                {
                    Content = "b" + i,
                    Height = 60,
                    Width = 60,
                    Margin = new Thickness(5),
                    FontSize = 10,
                }; sp1.Children.Add(button);
            }
        }
        SliderH.Value = ScrollViewer1.HorizontalOffset;
        SliderV.Value = ScrollViewer1.VerticalOffset;
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        SliderH.Maximum = ScrollViewer1.ScrollableWidth;
        SliderV.Maximum = ScrollViewer1.ScrollableHeight;
    }

    private void Animate()
    {
        if (double.IsNaN(MouseSpeed))
        { MouseSpeed = 0.0; }
        TimeSpan ts = TimeSpan.FromSeconds((MouseSpeed * scrollspeed));
        PowerEase Power = new PowerEase();
        {
            Power.Power = 15;
        }

        if (MouseSpeed < 0.4 || MouseSpeed > 500)
        {
            MouseSpeed = 0;
        }

        if (ScrollViewer1.ScrollableHeight > ScrollViewer1.ActualHeight)
        {
            if (Verticaloff1 < EndpointV)
            {
                Verticaloff1 = ScrollViewer1.VerticalOffset;
                EndV = Verticaloff1 + (ScrollViewer1.ScrollableHeight / (ScrollViewer1.ScrollableHeight / ScrollViewer1.ActualHeight / 2) * MouseSpeed / 2);
            }
            else
            {
                Verticaloff1 = ScrollViewer1.VerticalOffset;
                EndV = Verticaloff1 - (ScrollViewer1.ScrollableHeight / (ScrollViewer1.ScrollableHeight / ScrollViewer1.ActualHeight / 2) * MouseSpeed / 2);
            }

            DoubleAnimation DAV = new DoubleAnimation()
            {
                Duration = ts,
                From = Verticaloff1,
                To = EndV,
                EasingFunction = Power,
            };
            SliderV.BeginAnimation(Slider.ValueProperty, DAV);
        }

        if (ScrollViewer1.ScrollableWidth > ScrollViewer1.ActualWidth)
        {
            if (HorizontalOff1 < EndpointH)
            {
                HorizontalOff1 = ScrollViewer1.HorizontalOffset;
                EndH = HorizontalOff1 + (ScrollViewer1.ScrollableWidth / (ScrollViewer1.ScrollableWidth / ScrollViewer1.ActualWidth / 2) * MouseSpeed / 2);
            }
            else
            {
                HorizontalOff1 = ScrollViewer1.HorizontalOffset;
                EndH = HorizontalOff1 - (ScrollViewer1.ScrollableWidth / (ScrollViewer1.ScrollableWidth / ScrollViewer1.ActualWidth / 2) * MouseSpeed / 2);
            }

            DoubleAnimation DAH = new DoubleAnimation()
            {
                Duration = ts,
                From = HorizontalOff1,
                To = EndH,
                EasingFunction = Power,
            };
            SliderH.BeginAnimation(System.Windows.Controls.Primitives.RangeBase.ValueProperty, DAH);
        }
    }

    private void Slider1_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        ScrollViewer1.ScrollToHorizontalOffset(e.NewValue);
    }

    private void SliderV_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        ScrollViewer1.ScrollToVerticalOffset(e.NewValue);
    }
}

}


0

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

许多Sacha Barbers WPF代码已经集成到了一个Github项目中,用于WPF。请查看MahaApps Metro以获取一些非常有用的开源WPF实现。


6
我认为这并没有回答问题。原帖想要给滚动条添加动画效果,但"friction scrolling"(摩擦滚动)并不包含任何滚动条。请问需要添加什么内容? - McGarnagle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接