Animated (Smooth) scrolling on ScrollViewer
In your example there are two controls inherited from ScrollViewer
and ListBox
, the animation is implemented by SplineDoubleKeyFrame
[MSDN]. In my time, I realized animation scrolling via the attached dependency property VerticalOffsetProperty
, which allows you to directly transfer offset scrollbar into a double animation, like this:
DoubleAnimation verticalAnimation = new DoubleAnimation();
verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = some value;
verticalAnimation.Duration = new Duration( some duration );
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(verticalAnimation);
Storyboard.SetTarget(verticalAnimation, scrollViewer);
Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(ScrollAnimationBehavior.VerticalOffsetProperty)); // Attached dependency property
storyboard.Begin();
Examples can be found here:
How to: Animate the Horizontal/VerticalOffset properties of a ScrollViewer
WPF - Animate ListBox.ScrollViewer.HorizontalOffset?
In this case, works well smooth scrolling of the content and the Thumb
. Based on this approach, and using your example [How To Create An Animated ScrollViewer (or ListBox) in WPF], I created an attached behavior ScrollAnimationBehavior
, which can be applied to ScrollViewer
and ListBox
.
Example of using:
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
property is responsible for the scrolling animation for ScrollViewer
and for ListBox
. Below its implementation:
public static DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled",
typeof(bool),
typeof(ScrollAnimationBehavior),
new UIPropertyMetadata(false, OnIsEnabledChanged));
public static void SetIsEnabled(FrameworkElement target, bool value)
{
target.SetValue(IsEnabledProperty, value);
}
public static bool GetIsEnabled(FrameworkElement target)
{
return (bool)target.GetValue(IsEnabledProperty);
}
private static void OnIsEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var target = sender;
if (target != null && target is ScrollViewer)
{
ScrollViewer scroller = target as ScrollViewer;
scroller.Loaded += new RoutedEventHandler(scrollerLoaded);
}
if (target != null && target is ListBox)
{
ListBox listbox = target as ListBox;
listbox.Loaded += new RoutedEventHandler(listboxLoaded);
}
}
In these Loaded
handlers are set event handlers for PreviewMouseWheel
and PreviewKeyDown
.
Helpers (auxiliary procedures) are taken from the example and provide a value of double
type, which is passed to the procedure AnimateScroll()
. Here and are the magic key of animation:
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
The example only implemented vertical animation, if you will accept this project, you will realize itself without problems horizontal animation.
Selection of the current item in
ListBox
not transferred to the next element of this is due to the interception of eventsPreviewKeyDown
, so you have to think about this moment.This implementation is fully suited for the MVVM pattern. To use this behavior in the
Blend
, you need to inherit interfaceBehavior
. Example can be found here and here.
Tested on Windows XP, Windows Seven, .NET 4.0.
Sample project is available at this link.
Below is a full code of this implementation:
public static class ScrollAnimationBehavior
{
#region Private ScrollViewer for ListBox
private static ScrollViewer _listBoxScroller = new ScrollViewer();
#endregion
#region VerticalOffset Property
public static DependencyProperty VerticalOffsetProperty =
DependencyProperty.RegisterAttached("VerticalOffset",
typeof(double),
typeof(ScrollAnimationBehavior),
new UIPropertyMetadata(0.0, OnVerticalOffsetChanged));
public static void SetVerticalOffset(FrameworkElement target, double value)
{
target.SetValue(VerticalOffsetProperty, value);
}
public static double GetVerticalOffset(FrameworkElement target)
{
return (double)target.GetValue(VerticalOffsetProperty);
}
#endregion
#region TimeDuration Property
public static DependencyProperty TimeDurationProperty =
DependencyProperty.RegisterAttached("TimeDuration",
typeof(TimeSpan),
typeof(ScrollAnimationBehavior),
new PropertyMetadata(new TimeSpan(0, 0, 0, 0, 0)));
public static void SetTimeDuration(FrameworkElement target, TimeSpan value)
{
target.SetValue(TimeDurationProperty, value);
}
public static TimeSpan GetTimeDuration(FrameworkElement target)
{
return (TimeSpan)target.GetValue(TimeDurationProperty);
}
#endregion
#region PointsToScroll Property
public static DependencyProperty PointsToScrollProperty =
DependencyProperty.RegisterAttached("PointsToScroll",
typeof(double),
typeof(ScrollAnimationBehavior),
new PropertyMetadata(0.0));
public static void SetPointsToScroll(FrameworkElement target, double value)
{
target.SetValue(PointsToScrollProperty, value);
}
public static double GetPointsToScroll(FrameworkElement target)
{
return (double)target.GetValue(PointsToScrollProperty);
}
#endregion
#region OnVerticalOffset Changed
private static void OnVerticalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
ScrollViewer scrollViewer = target as ScrollViewer;
if (scrollViewer != null)
{
scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
}
}
#endregion
#region IsEnabled Property
public static DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled",
typeof(bool),
typeof(ScrollAnimationBehavior),
new UIPropertyMetadata(false, OnIsEnabledChanged));
public static void SetIsEnabled(FrameworkElement target, bool value)
{
target.SetValue(IsEnabledProperty, value);
}
public static bool GetIsEnabled(FrameworkElement target)
{
return (bool)target.GetValue(IsEnabledProperty);
}
#endregion
#region OnIsEnabledChanged Changed
private static void OnIsEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var target = sender;
if (target != null && target is ScrollViewer)
{
ScrollViewer scroller = target as ScrollViewer;
scroller.Loaded += new RoutedEventHandler(scrollerLoaded);
}
if (target != null && target is ListBox)
{
ListBox listbox = target as ListBox;
listbox.Loaded += new RoutedEventHandler(listboxLoaded);
}
}
#endregion
#region AnimateScroll Helper
private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
DoubleAnimation verticalAnimation = new DoubleAnimation();
verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = ToValue;
verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(verticalAnimation);
Storyboard.SetTarget(verticalAnimation, scrollViewer);
Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(ScrollAnimationBehavior.VerticalOffsetProperty));
storyboard.Begin();
}
#endregion
#region NormalizeScrollPos Helper
private static double NormalizeScrollPos(ScrollViewer scroll, double scrollChange, Orientation o)
{
double returnValue = scrollChange;
if (scrollChange < 0)
{
returnValue = 0;
}
if (o == Orientation.Vertical && scrollChange > scroll.ScrollableHeight)
{
returnValue = scroll.ScrollableHeight;
}
else if (o == Orientation.Horizontal && scrollChange > scroll.ScrollableWidth)
{
returnValue = scroll.ScrollableWidth;
}
return returnValue;
}
#endregion
#region UpdateScrollPosition Helper
private static void UpdateScrollPosition(object sender)
{
ListBox listbox = sender as ListBox;
if (listbox != null)
{
double scrollTo = 0;
for (int i = 0; i < (listbox.SelectedIndex); i++)
{
ListBoxItem tempItem = listbox.ItemContainerGenerator.ContainerFromItem(listbox.Items[i]) as ListBoxItem;
if (tempItem != null)
{
scrollTo += tempItem.ActualHeight;
}
}
AnimateScroll(_listBoxScroller, scrollTo);
}
}
#endregion
#region SetEventHandlersForScrollViewer Helper
private static void SetEventHandlersForScrollViewer(ScrollViewer scroller)
{
scroller.PreviewMouseWheel += new MouseWheelEventHandler(ScrollViewerPreviewMouseWheel);
scroller.PreviewKeyDown += new KeyEventHandler(ScrollViewerPreviewKeyDown);
}
#endregion
#region scrollerLoaded Event Handler
private static void scrollerLoaded(object sender, RoutedEventArgs e)
{
ScrollViewer scroller = sender as ScrollViewer;
SetEventHandlersForScrollViewer(scroller);
}
#endregion
#region listboxLoaded Event Handler
private static void listboxLoaded(object sender, RoutedEventArgs e)
{
ListBox listbox = sender as ListBox;
_listBoxScroller = FindVisualChildHelper.GetFirstChildOfType<ScrollViewer>(listbox);
SetEventHandlersForScrollViewer(_listBoxScroller);
SetTimeDuration(_listBoxScroller, new TimeSpan(0, 0, 0, 0, 200));
SetPointsToScroll(_listBoxScroller, 16.0);
listbox.SelectionChanged += new SelectionChangedEventHandler(ListBoxSelectionChanged);
listbox.Loaded += new RoutedEventHandler(ListBoxLoaded);
listbox.LayoutUpdated += new EventHandler(ListBoxLayoutUpdated);
}
#endregion
#region ScrollViewerPreviewMouseWheel Event Handler
private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
double mouseWheelChange = (double)e.Delta;
ScrollViewer scroller = (ScrollViewer)sender;
double newVOffset = GetVerticalOffset(scroller) - (mouseWheelChange / 3);
if (newVOffset < 0)
{
AnimateScroll(scroller, 0);
}
else if (newVOffset > scroller.ScrollableHeight)
{
AnimateScroll(scroller, scroller.ScrollableHeight);
}
else
{
AnimateScroll(scroller, newVOffset);
}
e.Handled = true;
}
#endregion
#region ScrollViewerPreviewKeyDown Handler
private static void ScrollViewerPreviewKeyDown(object sender, KeyEventArgs e)
{
ScrollViewer scroller = (ScrollViewer)sender;
Key keyPressed = e.Key;
double newVerticalPos = GetVerticalOffset(scroller);
bool isKeyHandled = false;
if (keyPressed == Key.Down)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + GetPointsToScroll(scroller)), Orientation.Vertical);
isKeyHandled = true;
}
else if (keyPressed == Key.PageDown)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + scroller.ViewportHeight), Orientation.Vertical);
isKeyHandled = true;
}
else if (keyPressed == Key.Up)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - GetPointsToScroll(scroller)), Orientation.Vertical);
isKeyHandled = true;
}
else if (keyPressed == Key.PageUp)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - scroller.ViewportHeight), Orientation.Vertical);
isKeyHandled = true;
}
if (newVerticalPos != GetVerticalOffset(scroller))
{
AnimateScroll(scroller, newVerticalPos);
}
e.Handled = isKeyHandled;
}
#endregion
#region ListBox Event Handlers
private static void ListBoxLayoutUpdated(object sender, EventArgs e)
{
UpdateScrollPosition(sender);
}
private static void ListBoxLoaded(object sender, RoutedEventArgs e)
{
UpdateScrollPosition(sender);
}
private static void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
UpdateScrollPosition(sender);
}
#endregion
}
For those getting here from google Anatoliy's code works, but has some issues with mouse wheel scrolling specifically.
Scrolling without Fix (Keep in mind i am attempting to rapid scroll to the bottom)
Scrolling with Fix
(self plug, you can find out what this application is over Here)
Let's take a look at why.
#region ScrollViewerPreviewMouseWheel Event Handler
private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
double mouseWheelChange = (double)e.Delta;
ScrollViewer scroller = (ScrollViewer)sender;
double newVOffset = GetVerticalOffset(scroller) - (mouseWheelChange / 3);
if (newVOffset < 0)
{
AnimateScroll(scroller, 0);
}
else if (newVOffset > scroller.ScrollableHeight)
{
AnimateScroll(scroller, scroller.ScrollableHeight);
}
else
{
AnimateScroll(scroller, newVOffset);
}
e.Handled = true;
}
In this handler code, you'll notice that it is called every time you scroll the mouse wheel. So when you hit it with rapid scrolling the animation doesn't have time to complete, and you are stuck attempting to scroll from where you are in the middle of the animation. This causes jittery slow scrolling when attempting to scroll faster.
Additionally their code here:
private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
DoubleAnimation verticalAnimation = new DoubleAnimation();
verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = ToValue;
verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(verticalAnimation);
Storyboard.SetTarget(verticalAnimation, scrollViewer);
Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(ScrollAnimationBehavior.VerticalOffsetProperty));
storyboard.Begin();
}
Has unneeded storyboard implementation, that can be removed to make the scroll animation interruptible, which is what we will need to in order to smooth out rapid scrolling.
At the top of their code we are going to add a new variable.
public static class ScrollAnimationBehavior
{
public static double intendedLocation = 0;
...
We need to save the intended location of the animation, so that if we call the scroller event again, we can jump to that location right away before starting the next animation call.
Now there's something else we have to change. The intendedLocation needs to be updated when the user either uses one of the keydown events (Page up or page down) or if they manually move the scrollbar with the mouse.
So we need to add another event to handle the left mouse button up on the scrollviewer, and when the mouse is lifted (put at intended location) then we can change the intended location, so that the scroll wheel gets the updated position.
private static void SetEventHandlersForScrollViewer(ScrollViewer scroller)
{
scroller.PreviewMouseWheel += new MouseWheelEventHandler(ScrollViewerPreviewMouseWheel);
scroller.PreviewKeyDown += new KeyEventHandler(ScrollViewerPreviewKeyDown);
scroller.PreviewMouseLeftButtonUp += Scroller_PreviewMouseLeftButtonUp;
}
private static void Scroller_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
intendedLocation = ((ScrollViewer)sender).VerticalOffset;
}
We still need to update the page up and page down area as well though.
private static void ScrollViewerPreviewKeyDown(object sender, KeyEventArgs e)
{
ScrollViewer scroller = (ScrollViewer)sender;
Key keyPressed = e.Key;
double newVerticalPos = GetVerticalOffset(scroller);
bool isKeyHandled = false;
if (keyPressed == Key.Down)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + GetPointsToScroll(scroller)), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.PageDown)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + scroller.ViewportHeight), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.Up)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - GetPointsToScroll(scroller)), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.PageUp)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - scroller.ViewportHeight), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
if (newVerticalPos != GetVerticalOffset(scroller))
{
intendedLocation = newVerticalPos;
AnimateScroll(scroller, newVerticalPos);
}
e.Handled = isKeyHandled;
}
Now that we have the non-mousewheel events handled to update the intended location, let's fix up the mouse wheel event.
private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
double mouseWheelChange = (double)e.Delta;
ScrollViewer scroller = (ScrollViewer)sender;
double newVOffset = intendedLocation - (mouseWheelChange * 2);
//Incase we got hit by the mouse again. jump to the offset.
scroller.ScrollToVerticalOffset(intendedLocation);
if (newVOffset < 0)
{
newVOffset = 0;
}
if (newVOffset > scroller.ScrollableHeight)
{
newVOffset = scroller.ScrollableHeight;
}
AnimateScroll(scroller, newVOffset);
intendedLocation = newVOffset;
e.Handled = true;
}
So the changes are as follows
Changed newVOffset to function from the intendedLocation and the mouseWheelChange.
Cleaned up when the newVOffset is over or under acceptable boundaries.
We jumped to the intendedLocation, which was created by the last scrollwheel event with where it wanted to go.
If you want to change the "speed" of the scroll simply change
double newVOffset = intendedLocation - (mouseWheelChange * 2);
You can change the modifier from times 2 to times 5 for faster or times 1 for slower.
With all the events handled now, let's make the animation cancel itself, so this all has a point.
private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
scrollViewer.BeginAnimation(VerticalOffsetProperty, null);
DoubleAnimation verticalAnimation = new DoubleAnimation();
verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = ToValue;
verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));
scrollViewer.BeginAnimation(VerticalOffsetProperty, verticalAnimation);
}
So what we did here was remove the storyboard, and nullified any existing animation so that we can start with a fresh new one.
Below is the full code (Provided AS IS), incase you're too lazy to change it like I was just copying it.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Animation;
using System.Windows.Input;
using ScrollAnimateBehavior.Helpers;
namespace ScrollAnimateBehavior.AttachedBehaviors
{
public static class ScrollAnimationBehavior
{
public static double intendedLocation = 0;
#region Private ScrollViewer for ListBox
private static ScrollViewer _listBoxScroller = new ScrollViewer();
#endregion
#region VerticalOffset Property
public static DependencyProperty VerticalOffsetProperty =
DependencyProperty.RegisterAttached("VerticalOffset",
typeof(double),
typeof(ScrollAnimationBehavior),
new UIPropertyMetadata(0.0, OnVerticalOffsetChanged));
public static void SetVerticalOffset(FrameworkElement target, double value)
{
target.SetValue(VerticalOffsetProperty, value);
}
public static double GetVerticalOffset(FrameworkElement target)
{
return (double)target.GetValue(VerticalOffsetProperty);
}
#endregion
#region TimeDuration Property
public static DependencyProperty TimeDurationProperty =
DependencyProperty.RegisterAttached("TimeDuration",
typeof(TimeSpan),
typeof(ScrollAnimationBehavior),
new PropertyMetadata(new TimeSpan(0, 0, 0, 0, 0)));
public static void SetTimeDuration(FrameworkElement target, TimeSpan value)
{
target.SetValue(TimeDurationProperty, value);
}
public static TimeSpan GetTimeDuration(FrameworkElement target)
{
return (TimeSpan)target.GetValue(TimeDurationProperty);
}
#endregion
#region PointsToScroll Property
public static DependencyProperty PointsToScrollProperty =
DependencyProperty.RegisterAttached("PointsToScroll",
typeof(double),
typeof(ScrollAnimationBehavior),
new PropertyMetadata(0.0));
public static void SetPointsToScroll(FrameworkElement target, double value)
{
target.SetValue(PointsToScrollProperty, value);
}
public static double GetPointsToScroll(FrameworkElement target)
{
return (double)target.GetValue(PointsToScrollProperty);
}
#endregion
#region OnVerticalOffset Changed
private static void OnVerticalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
ScrollViewer scrollViewer = target as ScrollViewer;
if (scrollViewer != null)
{
scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
}
}
#endregion
#region IsEnabled Property
public static DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled",
typeof(bool),
typeof(ScrollAnimationBehavior),
new UIPropertyMetadata(false, OnIsEnabledChanged));
public static void SetIsEnabled(FrameworkElement target, bool value)
{
target.SetValue(IsEnabledProperty, value);
}
public static bool GetIsEnabled(FrameworkElement target)
{
return (bool)target.GetValue(IsEnabledProperty);
}
#endregion
#region OnIsEnabledChanged Changed
private static void OnIsEnabledChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var target = sender;
if (target != null && target is ScrollViewer)
{
ScrollViewer scroller = target as ScrollViewer;
scroller.Loaded += new RoutedEventHandler(scrollerLoaded);
}
if (target != null && target is ListBox)
{
ListBox listbox = target as ListBox;
listbox.Loaded += new RoutedEventHandler(listboxLoaded);
}
}
#endregion
#region AnimateScroll Helper
private static void AnimateScroll(ScrollViewer scrollViewer, double ToValue)
{
scrollViewer.BeginAnimation(VerticalOffsetProperty, null);
DoubleAnimation verticalAnimation = new DoubleAnimation();
verticalAnimation.From = scrollViewer.VerticalOffset;
verticalAnimation.To = ToValue;
verticalAnimation.Duration = new Duration(GetTimeDuration(scrollViewer));
scrollViewer.BeginAnimation(VerticalOffsetProperty, verticalAnimation);
}
#endregion
#region NormalizeScrollPos Helper
private static double NormalizeScrollPos(ScrollViewer scroll, double scrollChange, Orientation o)
{
double returnValue = scrollChange;
if (scrollChange < 0)
{
returnValue = 0;
}
if (o == Orientation.Vertical && scrollChange > scroll.ScrollableHeight)
{
returnValue = scroll.ScrollableHeight;
}
else if (o == Orientation.Horizontal && scrollChange > scroll.ScrollableWidth)
{
returnValue = scroll.ScrollableWidth;
}
return returnValue;
}
#endregion
#region UpdateScrollPosition Helper
private static void UpdateScrollPosition(object sender)
{
ListBox listbox = sender as ListBox;
if (listbox != null)
{
double scrollTo = 0;
for (int i = 0; i < (listbox.SelectedIndex); i++)
{
ListBoxItem tempItem = listbox.ItemContainerGenerator.ContainerFromItem(listbox.Items[i]) as ListBoxItem;
if (tempItem != null)
{
scrollTo += tempItem.ActualHeight;
}
}
AnimateScroll(_listBoxScroller, scrollTo);
}
}
#endregion
#region SetEventHandlersForScrollViewer Helper
private static void SetEventHandlersForScrollViewer(ScrollViewer scroller)
{
scroller.PreviewMouseWheel += new MouseWheelEventHandler(ScrollViewerPreviewMouseWheel);
scroller.PreviewKeyDown += new KeyEventHandler(ScrollViewerPreviewKeyDown);
scroller.PreviewMouseLeftButtonUp += Scroller_PreviewMouseLeftButtonUp;
}
private static void Scroller_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
intendedLocation = ((ScrollViewer)sender).VerticalOffset;
}
#endregion
#region scrollerLoaded Event Handler
private static void scrollerLoaded(object sender, RoutedEventArgs e)
{
ScrollViewer scroller = sender as ScrollViewer;
SetEventHandlersForScrollViewer(scroller);
}
#endregion
#region listboxLoaded Event Handler
private static void listboxLoaded(object sender, RoutedEventArgs e)
{
ListBox listbox = sender as ListBox;
_listBoxScroller = FindVisualChildHelper.GetFirstChildOfType<ScrollViewer>(listbox);
SetEventHandlersForScrollViewer(_listBoxScroller);
SetTimeDuration(_listBoxScroller, new TimeSpan(0, 0, 0, 0, 200));
SetPointsToScroll(_listBoxScroller, 16.0);
listbox.SelectionChanged += new SelectionChangedEventHandler(ListBoxSelectionChanged);
listbox.Loaded += new RoutedEventHandler(ListBoxLoaded);
listbox.LayoutUpdated += new EventHandler(ListBoxLayoutUpdated);
}
#endregion
#region ScrollViewerPreviewMouseWheel Event Handler
private static void ScrollViewerPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
double mouseWheelChange = (double)e.Delta;
ScrollViewer scroller = (ScrollViewer)sender;
double newVOffset = intendedLocation - (mouseWheelChange * 2);
//We got hit by the mouse again. jump to the offset.
scroller.ScrollToVerticalOffset(intendedLocation);
if (newVOffset < 0)
{
newVOffset = 0;
}
if (newVOffset > scroller.ScrollableHeight)
{
newVOffset = scroller.ScrollableHeight;
}
AnimateScroll(scroller, newVOffset);
intendedLocation = newVOffset;
e.Handled = true;
}
#endregion
#region ScrollViewerPreviewKeyDown Handler
private static void ScrollViewerPreviewKeyDown(object sender, KeyEventArgs e)
{
ScrollViewer scroller = (ScrollViewer)sender;
Key keyPressed = e.Key;
double newVerticalPos = GetVerticalOffset(scroller);
bool isKeyHandled = false;
if (keyPressed == Key.Down)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + GetPointsToScroll(scroller)), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.PageDown)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos + scroller.ViewportHeight), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.Up)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - GetPointsToScroll(scroller)), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
else if (keyPressed == Key.PageUp)
{
newVerticalPos = NormalizeScrollPos(scroller, (newVerticalPos - scroller.ViewportHeight), Orientation.Vertical);
intendedLocation = newVerticalPos;
isKeyHandled = true;
}
if (newVerticalPos != GetVerticalOffset(scroller))
{
intendedLocation = newVerticalPos;
AnimateScroll(scroller, newVerticalPos);
}
e.Handled = isKeyHandled;
}
#endregion
#region ListBox Event Handlers
private static void ListBoxLayoutUpdated(object sender, EventArgs e)
{
UpdateScrollPosition(sender);
}
private static void ListBoxLoaded(object sender, RoutedEventArgs e)
{
UpdateScrollPosition(sender);
}
private static void ListBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
UpdateScrollPosition(sender);
}
#endregion
}
}