2013-12-22 103 views
23

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

我试图寻找在互联网上,我发现的唯一的事情是这样的:

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

它的工作原理相当不错,但我有一个问题 - 这动画滚动效果但ScrollViewerThumb直接去按点 - 我希望它是藏汉动画

我怎样才能使ScrollViewerThumb被藏汉动画,否则是没有用相同的亲工作控制perties /我想要的功能?

+0

唉......在链路的整个做法是非常hackish的:它复制的控制和DPS保持现有的控制行为,同时加入动画。我尝试了一些方法来获得所需的行为(在“animateScroller”中为动画滚动条制作动画,使“PART_AniVerticalScrollBar”成为双向绑定),但每种方式都会陷入奇怪的行为。 – McGarnagle

+1

我最好的建议是重新编写整个'ScrollViewer'控件。我意识到这是有点棘手的...但子类化现有的控制在我看来太麻烦了,因为缺乏动画被烘烤。 – McGarnagle

+0

我创建了我自己的控件,由两个ScrollBar和一个ScrollViewer组成隐藏滚动条,将滚动条从滚动查看器中分离出来。然后,我可以轻松实现摩擦滚动,因为我手动处理滚动条拖动。 –

回答

36

在您的例子有来自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

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

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

滚动自定义的最佳示例可以在代码项目的Sacha Barber文章中找到。看到这个code project article on Friction scrolling关于这个话题的文章。

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

+5

我不认为这真的解决了这个问题? OP想要动画滚动条,但“摩擦滚动”的东西根本没有滚动条。 – McGarnagle