How to Implement Smooth Scroll in a Wpf Listview

ListBox, VirtualizingStackPanel, and Smooth Scrolling in WPF

When you uncheck CanContentScroll, you lose virtualization. And the answer is really frustrating: For now there is no out-of-the-box solution :(.

PS: This is not the first post here, asking this very question.

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

Sample Image

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 events PreviewKeyDown, 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 interface Behavior. 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
}

WinForms ListView smooth scrolling

Being able to scroll by a partial line is not generally a useful feature.

However, if you really want to be able to do it, just set ShowGroups to true. The ListView control will then allow pixel level scrolling.

For example:

example of pixel level scrolling



Related Topics



Leave a reply



Submit