In this article I will demonstrate how to implement a WPF Scroll Viewer that scrolls smoothly.
By default, WPF scrolling is not smooth/animated. I.e. it jumps between the scroll steps. There are some solutions around to add animation, but I didn’t find a properly solution for me so I decided to implement an own one. I tried to find an easy implementation which just adds animation without changing the default scrolling behavior and without adding a new control template. I found out that there is an ideal place to hook in: between the ScrollViewer and the IScrollInfo (i.e. the panel) counterpart. So I implemented an adapter class which is hooked in on loading time of the ScrollViewer. This also allows us to add the smooth scrolling functionality using a behavior. However, a drawback when using a behavior is that the ScrollInfo property is protected and needs to be accessed using reflection. If you don’t want to do that you can create a derivation from ScrollViewer, add an event handler for the Loaded event and add the adapter.
Hint: the animated scrolling is only working when scrolling is performed by physical units (i.e. CanContentScroll is set to false). When CanContentScroll = true the animation is automatically switched off!
The Scroll Info adapter is quite easy:
using System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace ScrollViewer
{
public class ScrollInfoAdapter : UIElement, IScrollInfo
{
private IScrollInfo _child;
private double _computedVerticalOffset = 0;
private double _computedHorizontalOffset = 0;
internal const double _scrollLineDelta = 16.0;
internal const double _mouseWheelDelta = 48.0;
public ScrollInfoAdapter(IScrollInfo child)
{
_child = child;
}
public bool CanVerticallyScroll
{
get => _child.CanVerticallyScroll;
set => _child.CanVerticallyScroll = value;
}
public bool CanHorizontallyScroll
{
get => _child.CanHorizontallyScroll;
set => _child.CanHorizontallyScroll = value;
}
public double ExtentWidth => _child.ExtentWidth;
public double ExtentHeight => _child.ExtentHeight;
public double ViewportWidth => _child.ViewportWidth;
public double ViewportHeight => _child.ViewportHeight;
public double HorizontalOffset => _child.HorizontalOffset;
public double VerticalOffset => _child.VerticalOffset;
public System.Windows.Controls.ScrollViewer ScrollOwner
{
get => _child.ScrollOwner;
set => _child.ScrollOwner = value;
}
public Rect MakeVisible(Visual visual, Rect rectangle)
{
return _child.MakeVisible(visual, rectangle);
}
public void LineUp()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.LineUp();
else
VerticalScroll(_computedVerticalOffset - _scrollLineDelta);
}
public void LineDown()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.LineDown();
else
VerticalScroll(_computedVerticalOffset + _scrollLineDelta);
}
public void LineLeft()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.LineLeft();
else
HorizontalScroll(_computedHorizontalOffset - _scrollLineDelta);
}
public void LineRight()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.LineRight();
else
HorizontalScroll(_computedHorizontalOffset + _scrollLineDelta);
}
public void MouseWheelUp()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.MouseWheelUp();
else
VerticalScroll(_computedVerticalOffset - _mouseWheelDelta);
}
public void MouseWheelDown()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.MouseWheelDown();
else
VerticalScroll(_computedVerticalOffset + _mouseWheelDelta);
}
public void MouseWheelLeft()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.MouseWheelLeft();
else
HorizontalScroll(_computedHorizontalOffset - _mouseWheelDelta);
}
public void MouseWheelRight()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.MouseWheelRight();
else
HorizontalScroll(_computedHorizontalOffset + _mouseWheelDelta);
}
public void PageUp()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.PageUp();
else
VerticalScroll(_computedVerticalOffset - ViewportHeight);
}
public void PageDown()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.PageDown();
else
VerticalScroll(_computedVerticalOffset + ViewportHeight);
}
public void PageLeft()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.PageLeft();
else
HorizontalScroll(_computedHorizontalOffset - ViewportWidth);
}
public void PageRight()
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.PageRight();
else
HorizontalScroll(_computedHorizontalOffset + ViewportWidth);
}
public void SetHorizontalOffset(double offset)
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.SetHorizontalOffset(offset);
else
{
_computedHorizontalOffset = offset;
Animate(HorizontalScrollOffsetProperty, offset, 0);
}
}
public void SetVerticalOffset(double offset)
{
if (_child.ScrollOwner.CanContentScroll == true)
_child.SetVerticalOffset(offset);
else
{
_computedVerticalOffset = offset;
Animate(VerticalScrollOffsetProperty, offset, 0);
}
}
#region not exposed methods
private void Animate(DependencyProperty property, double targetValue, int duration = 300)
{
//make a smooth animation that starts and ends slowly
var keyFramesAnimation = new DoubleAnimationUsingKeyFrames();
keyFramesAnimation.Duration = TimeSpan.FromMilliseconds(duration);
keyFramesAnimation.KeyFrames.Add(
new SplineDoubleKeyFrame(
targetValue,
KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(duration)),
new KeySpline(0.5, 0.0, 0.5, 1.0)
)
);
BeginAnimation(property, keyFramesAnimation);
}
private void VerticalScroll(double val)
{
if (Math.Abs(_computedVerticalOffset - ValidateVerticalOffset(val)) > 0.1)//prevent restart of animation in case of frequent event fire
{
_computedVerticalOffset = ValidateVerticalOffset(val);
Animate(VerticalScrollOffsetProperty, _computedVerticalOffset);
}
}
private void HorizontalScroll(double val)
{
if (Math.Abs(_computedHorizontalOffset - ValidateHorizontalOffset(val)) > 0.1)//prevent restart of animation in case of frequent event fire
{
_computedHorizontalOffset = ValidateHorizontalOffset(val);
Animate(HorizontalScrollOffsetProperty, _computedHorizontalOffset);
}
}
private double ValidateVerticalOffset(double verticalOffset)
{
if (verticalOffset < 0)
return 0;
if (verticalOffset > _child.ScrollOwner.ScrollableHeight)
return _child.ScrollOwner.ScrollableHeight;
return verticalOffset;
}
private double ValidateHorizontalOffset(double horizontalOffset)
{
if (horizontalOffset < 0)
return 0;
if (horizontalOffset > _child.ScrollOwner.ScrollableWidth)
return _child.ScrollOwner.ScrollableWidth;
return horizontalOffset;
}
#endregion
#region helper dependency properties as scrollbars are not animatable by default
internal double VerticalScrollOffset
{
get { return (double)GetValue(VerticalScrollOffsetProperty); }
set { SetValue(VerticalScrollOffsetProperty, value); }
}
internal static readonly DependencyProperty VerticalScrollOffsetProperty =
DependencyProperty.Register("VerticalScrollOffset", typeof(double), typeof(ScrollInfoAdapter),
new PropertyMetadata(0.0, new PropertyChangedCallback(OnVerticalScrollOffsetChanged)));
private static void OnVerticalScrollOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var smoothScrollViewer = (ScrollInfoAdapter)d;
smoothScrollViewer._child.SetVerticalOffset((double)e.NewValue);
}
internal double HorizontalScrollOffset
{
get { return (double)GetValue(HorizontalScrollOffsetProperty); }
set { SetValue(HorizontalScrollOffsetProperty, value); }
}
internal static readonly DependencyProperty HorizontalScrollOffsetProperty =
DependencyProperty.Register("HorizontalScrollOffset", typeof(double), typeof(ScrollInfoAdapter),
new PropertyMetadata(0.0, new PropertyChangedCallback(OnHorizontalScrollOffsetChanged)));
private static void OnHorizontalScrollOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var smoothScrollViewer = (ScrollInfoAdapter)d;
smoothScrollViewer._child.SetHorizontalOffset((double)e.NewValue);
}
#endregion
}
}
Code language: C# (cs)
This adapter can be added be derivating from ScrollViewer or be means of an Behavior.
using System.Windows;
namespace ScrollViewer
{
public class SmoothScrollViewer : System.Windows.Controls.ScrollViewer
{
public SmoothScrollViewer()
{
Loaded += ScrollViewer_Loaded;
}
private void ScrollViewer_Loaded(object sender, RoutedEventArgs e)
{
ScrollInfo = new ScrollInfoAdapter(ScrollInfo);
}
}
}
Code language: C# (cs)
using Microsoft.Xaml.Behaviors;
using System.Reflection;
using System.Windows.Controls.Primitives;
namespace ScrollViewer
{
public class SmoothScrollViewerBehavior : Behavior<System.Windows.Controls.ScrollViewer>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += ScrollViewerLoaded;
}
private void ScrollViewerLoaded(object sender, System.Windows.RoutedEventArgs e)
{
var property = AssociatedObject.GetType().GetProperty("ScrollInfo", BindingFlags.NonPublic | BindingFlags.Instance);
property.SetValue(AssociatedObject, new ScrollInfoAdapter((IScrollInfo)property.GetValue(AssociatedObject)));
}
}
}
Code language: C# (cs)
Thanks for sharing your thoughts about . Regards
NO matter what I do i have error Severity
Error XDG0008 The name “SmoothScrollViewerBehavior” does not exist in the namespace “clr-namespace:ScrollViewer;assembly=ScrollViewer”.
PLease can helpme?
Hi!
have you copied the SmoothScrollViewerBehavior definition to your project?
Best regards,
Thomas