【问题标题】:Animated (Smooth) scrolling on ScrollViewerScrollViewer 上的动画(平滑)滚动
【发布时间】:2013-12-22 16:17:20
【问题描述】:

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

我尝试在互联网上搜索,我发现的唯一内容是:

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

它工作得很好,但我有一个问题 - 它可以动画滚动效果,但 ScrollViewerThumb 直接指向按下的点 - 我希望它也有动画效果

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

【问题讨论】:

  • 呃...链接上的整个方法非常骇人听闻:它复制控件和 DP 以保持现有的控件行为,同时添加动画。我尝试了几种方法来获得所需的行为(在“animateScroller”中为滚动条设置动画,使“PART_AniVerticalScrollBar”成为双向绑定),但每种方式都遇到了奇怪的行为。
  • 我最好的建议是从头开始重写整个ScrollViewer 控件。我意识到这有点棘手......但是对现有控件进行子类化在我看来太成问题了,因为缺少动画。
  • 我创建了自己的控件,由两个 ScrollBars 和一个带有隐藏滚动条的 ScrollViewer 组成,用于将滚动条与滚动查看器分开。然后我可以轻松实现摩擦滚动,因为我手动处理滚动条拖动。

标签: wpf animation scrollviewer


【解决方案1】:

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

可以在此处找到示例:

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

WPF - Animate ListBox.ScrollViewer.HorizontalOffset?

在这种情况下,内容和Thumb 的平滑滚动效果很好。基于这种方法,并使用您的示例 [How To Create An Animated ScrollViewer (or ListBox) in WPF],我创建了一个附加行为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>

Output

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

Some notes

  • 这个例子只实现了竖屏动画,如果你接受这个项目,你将自己实现横屏动画没有问题。

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

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

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


link 提供示例项目。


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

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
}

【讨论】:

    【解决方案2】:

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

    Scrolling without Fix(请记住,我正在尝试快速滚动到底部)

    Scrolling with Fix

    (自插,你可以查一下这个应用是什么结束Here)

    让我们来看看为什么。

    #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 事件之一(向上翻页或向下翻页)或使用鼠标手动移动滚动条时,需要更新预期位置。

    所以我们需要添加另一个事件来处理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 更改为根据预期位置和 mouseWheelChange 运行。

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

    3. 我们跳到了预期的位置,它是由最后一个滚轮事件创建的,带有它想去的地方。

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

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

    【讨论】:

      【解决方案3】:

      滚动自定义的最佳示例可以在 Sacha Barber 在 Code Project 上的一篇文章中找到。请参阅这篇关于该主题的code project article on Friction scrolling 文章。

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

      【讨论】:

      • 我不认为这真的解决了这个问题? OP 想要动画滚动条,但是“摩擦滚动”的东西根本没有滚动条。
      【解决方案4】:

      在解决了一些依赖问题后,我决定使用滑块对其进行硬编码以克服只读问题。对于这里感兴趣的人来说,这是所写的。 我还实现了鼠标速度,因此滚动越快,滚动越远。如果有人可以从中创建用户控件,我会很高兴。

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

      }

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多