Question

I have a ScrollViewer in my WPF App, and I want it to have smooth/animated scrolling effect just like Firefox has (if you know what I am talking about).

I tried to search over the internet, and the only thing I've found is this:

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

It works pretty good, but I have one problem with it - it animates the scrolling effect but the ScrollViewer's Thumb goes directly to the point pressed - I want it to be animated aswell

How can I cause the ScrollViewer's Thumb to be animated as well, or else is there a working control with the same properties/features I want?

Was it helpful?

Solution

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

enter image description here

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
}

OTHER TIPS

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

  1. Changed newVOffset to function from the intendedLocation and the mouseWheelChange.

  2. Cleaned up when the newVOffset is over or under acceptable boundaries.

  3. 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
    }
}

The best example of scroll customization can be found in an article by Sacha Barber on Code Project. See this code project article on Friction scrolling article on the topic.

A number of Sacha Barbers WPF code has been integrated into a Github project for WPF. See MahaApps Metro for some very useful open source WPF implementations.

after some struggling with dependencies i decided to hardcode it with the use sliders to overcome the readonly. for the ppl interested here is what is wrote. i also implemented mousespeed so the faster you scroll the further the scroll extends. and if there is someone who can create an usercontrol out of it, i would be glad.

<Window x:Class="scrolltest4.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800" Loaded="Window_Loaded">

<Grid>

    <TextBox x:Name="tb3" HorizontalAlignment="Left" Margin="100,292,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="120" />
    <Slider x:Name="SliderH" HorizontalAlignment="Left" ValueChanged="Slider1_ValueChanged" Margin="282,335,0,0" VerticalAlignment="Top" Width="265" Height="21" />
    <Slider x:Name="SliderV" HorizontalAlignment="Left" Margin="753,95,0,0" Orientation="Vertical" IsDirectionReversed="True" VerticalAlignment="Top" Width="18" Height="206" FlowDirection="LeftToRight" ValueChanged="SliderV_ValueChanged" />
    <ScrollViewer x:Name="ScrollViewer1" ScrollChanged="ScrollViewer1_ScrollChanged" Width="auto" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" Margin="129,67,100,217"
                  PreviewMouseLeftButtonDown="ScrollViewer_PreviewMouseLeftButtonDown" PreviewMouseMove="ScrollViewer_PreviewMouseMove" PreviewMouseLeftButtonUp="ScrollViewer_PreviewMouseLeftButtonUp" Background="#FFFFBABA" HorizontalContentAlignment="Center" BorderThickness="2,2,2,2">
        <ScrollViewer.OpacityMask>
            <RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" SpreadMethod="Pad">
                <RadialGradientBrush.RelativeTransform>
                    <TransformGroup>
                        <ScaleTransform CenterY="0.5" CenterX="0.5" ScaleY="1" ScaleX="1" />
                    </TransformGroup>
                </RadialGradientBrush.RelativeTransform>
                <GradientStop Color="Black" Offset="0.532" />
                <GradientStop Offset="1" />
                <GradientStop Color="#FF161616" Offset="0.761" />
            </RadialGradientBrush>
        </ScrollViewer.OpacityMask>
        <StackPanel x:Name="sp1" Orientation="Horizontal" />
    </ScrollViewer>
</Grid>
    using System;
    using System.Collections;
    using System.Diagnostics;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media.Animation;

   namespace scrolltest4
   {

public partial class MainWindow : Window
{
    public System.Windows.Point ScrollMousePoint1 = new System.Windows.Point();
    private Queue TimePoints;
    private double HorizontalOff1 = 1;
    private double Verticaloff1 = 1;
    private double MouseSpeed = 0.1;
    private Stopwatch SW = new Stopwatch();
    private double scrollspeed = 2;

    private double EndV;
    private double EndH;
    private double EndpointH;
    private double EndpointV;

    public MainWindow()
    {
        TimePoints = new Queue(100);
        InitializeComponent();
        Buildstackpanel();
    }

    private void Buildstackpanel()
    {
        int i;

        for (i = 0; i < 80; i++)
        {
            Button button = new Button()
            {
                Content = "b" + i,
                Height = 60,
                Width = 60,
                Margin = new Thickness(5),
                FontSize = 10,
            }; sp1.Children.Add(button);
        }
    }

    private void ScrollViewer_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        SW.Reset();
        SW.Start();
        ScrollMousePoint1 = e.GetPosition(ScrollViewer1);
        HorizontalOff1 = ScrollViewer1.HorizontalOffset;
        Verticaloff1 = ScrollViewer1.HorizontalOffset;
        ScrollViewer1.CaptureMouse();
    }

    private void ScrollViewer_PreviewMouseMove(object sender, MouseEventArgs e)
    {
        if (ScrollViewer1.IsMouseCaptured)
        {
            AddPoint();
            ScrollViewer1.ScrollToHorizontalOffset(HorizontalOff1 + (ScrollMousePoint1.X - e.GetPosition(ScrollViewer1).X));
            ScrollViewer1.ScrollToVerticalOffset(Verticaloff1 + (ScrollMousePoint1.Y - e.GetPosition(ScrollViewer1).Y));
        }
    }

    private void ScrollViewer_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        TimePoints.Clear();
        ScrollViewer1.ReleaseMouseCapture();
        EndpointH = ScrollViewer1.HorizontalOffset;
        EndpointV = ScrollViewer1.VerticalOffset;
        SW.Stop();
        Animate();
    }

    private void AddPoint()
    {
        TimePoints.Enqueue(new TimePoint(Mouse.GetPosition(ScrollViewer1), SW.ElapsedMilliseconds));
        if (TimePoints.Count == 100) TimePoints.Dequeue();
        object[] array = TimePoints.ToArray();
        TimePoint tip = (TimePoint)array[array.Length - 1];
        TimePoint end = (TimePoint)array[0];
        double deltaX = (tip.point.X - end.point.X);
        double deltaY = (tip.point.Y - end.point.Y);
        double distance = deltaX * deltaX + deltaY * deltaY;
        long deltaT = tip.time - end.time;
        MouseSpeed = Math.Sqrt(distance) / SW.ElapsedMilliseconds;
        double velocity_X = deltaX / (double)deltaT;
        double velocity_Y = deltaY / (double)deltaT;
        tb3.Text = string.Format("|V| = {0}; Vx = {1}; Vy = {2}", MouseSpeed, velocity_X, velocity_Y);
    }

    public class TimePoint
    {
        public Point point;
        public long time;

        public TimePoint(Point pt, long ms)
        {
            point = pt;
            time = ms;
        }
    }

    private void ScrollViewer1_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (ScrollViewer1.HorizontalOffset > ScrollViewer1.ScrollableWidth * 0.95)
        {
            for (int i = 0; i < 3; i++)
            {
                Button button = new Button()
                {
                    Content = "b" + i,
                    Height = 60,
                    Width = 60,
                    Margin = new Thickness(5),
                    FontSize = 10,
                }; sp1.Children.Add(button);
            }
        }
        SliderH.Value = ScrollViewer1.HorizontalOffset;
        SliderV.Value = ScrollViewer1.VerticalOffset;
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        SliderH.Maximum = ScrollViewer1.ScrollableWidth;
        SliderV.Maximum = ScrollViewer1.ScrollableHeight;
    }

    private void Animate()
    {
        if (double.IsNaN(MouseSpeed))
        { MouseSpeed = 0.0; }
        TimeSpan ts = TimeSpan.FromSeconds((MouseSpeed * scrollspeed));
        PowerEase Power = new PowerEase();
        {
            Power.Power = 15;
        }

        if (MouseSpeed < 0.4 || MouseSpeed > 500)
        {
            MouseSpeed = 0;
        }

        if (ScrollViewer1.ScrollableHeight > ScrollViewer1.ActualHeight)
        {
            if (Verticaloff1 < EndpointV)
            {
                Verticaloff1 = ScrollViewer1.VerticalOffset;
                EndV = Verticaloff1 + (ScrollViewer1.ScrollableHeight / (ScrollViewer1.ScrollableHeight / ScrollViewer1.ActualHeight / 2) * MouseSpeed / 2);
            }
            else
            {
                Verticaloff1 = ScrollViewer1.VerticalOffset;
                EndV = Verticaloff1 - (ScrollViewer1.ScrollableHeight / (ScrollViewer1.ScrollableHeight / ScrollViewer1.ActualHeight / 2) * MouseSpeed / 2);
            }

            DoubleAnimation DAV = new DoubleAnimation()
            {
                Duration = ts,
                From = Verticaloff1,
                To = EndV,
                EasingFunction = Power,
            };
            SliderV.BeginAnimation(Slider.ValueProperty, DAV);
        }

        if (ScrollViewer1.ScrollableWidth > ScrollViewer1.ActualWidth)
        {
            if (HorizontalOff1 < EndpointH)
            {
                HorizontalOff1 = ScrollViewer1.HorizontalOffset;
                EndH = HorizontalOff1 + (ScrollViewer1.ScrollableWidth / (ScrollViewer1.ScrollableWidth / ScrollViewer1.ActualWidth / 2) * MouseSpeed / 2);
            }
            else
            {
                HorizontalOff1 = ScrollViewer1.HorizontalOffset;
                EndH = HorizontalOff1 - (ScrollViewer1.ScrollableWidth / (ScrollViewer1.ScrollableWidth / ScrollViewer1.ActualWidth / 2) * MouseSpeed / 2);
            }

            DoubleAnimation DAH = new DoubleAnimation()
            {
                Duration = ts,
                From = HorizontalOff1,
                To = EndH,
                EasingFunction = Power,
            };
            SliderH.BeginAnimation(System.Windows.Controls.Primitives.RangeBase.ValueProperty, DAH);
        }
    }

    private void Slider1_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        ScrollViewer1.ScrollToHorizontalOffset(e.NewValue);
    }

    private void SliderV_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        ScrollViewer1.ScrollToVerticalOffset(e.NewValue);
    }
}

}

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top