Анимированная (плавная) прокрутка на ScrollViewer
У меня есть ScrollViewer в моем приложении WPF, и я хочу, чтобы он имел плавный/анимированный эффект прокрутки, как и Firefox (Если вы знаете, о чем я говорю).
Я попытался поискать в интернете, и единственное, что я нашел, это:
Как создать анимированный ScrollViewer (или ListBox) в WPF
Он работает довольно хорошо, но у меня есть одна проблема с ним - он анимирует эффект прокрутки, но ScrollViewer ' s Thumb идет прямо к нажатой точке - Я хочу, чтобы он был одушевлен также
Как я могу заставить ScrollViewer's Thumb быть анимированным, или же есть рабочий элемент управления с теми же свойствами/функциями, которые я хочу?
2 ответов:
В вашем примере есть два элемента управления, унаследованные от
ScrollViewerиListBox, анимация реализуется с помощью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
WPF-анимация список.ScrollViewer.Горизонтальное смещение?
В этом случае хорошо работает плавная прокрутка содержимого и самого
Thumb. Основываясь на этом подходе и используя Ваш пример [Как создать анимированный ScrollViewer (или ListBox) в WPF] , я создал прикрепленное поведениеScrollAnimationBehavior, которое может быть применено кScrollViewerиListBox.Пример использования:
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собственность отвечает за прокрутку анимация дляScrollViewerи дляListBox. Ниже его реализация: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обработчиках задаются обработчики событий дляPreviewMouseWheelиPreviewKeyDown.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. Пример можно найти здесь и здесь .
Tested on 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 }
Лучший пример настройки прокрутки можно найти в статьеSacha Barber о проекте Code. Смотрите эту статью проекта кода по трению прокрутки по теме.
Ряд Sacha Barbers WPF code был интегрирован в проект Github для WPF. Смотрите MahaApps Metro для некоторых очень полезных реализаций WPF с открытым исходным кодом.

Comments