4

Animating the scrolling in a ScrollViewer seems to be a common task. I implemented it using a timer, similar to the approach found here. This method was working great, it was very smooth and looked perfect.

However, now that the complexity and number of objects contained within my ScrollViewer has increased, the animation looks very jerky. I find this odd because it works fine if I scroll manually.

    public void ShiftLeft(int speed = 11)
    {
        CustomTimer timer = new CustomTimer(); //DispatchTimer with "life"
        timer.Interval = new TimeSpan(0, 0, 0, 0, 5);
        timer.Tick += ((sender, e) =>
        {
            scrollViewer1.ScrollToHorizontalOffset(
                scrollViewer1.HorizontalOffset - (scrollViewer1.ScrollableWidth / (gridColumnCount - 3) / speed));
            if (scrollViewer1.HorizontalOffset == 0) //cant scroll any more
                ((CustomTimer)sender).Stop();
            ((CustomTimer)sender).life++;
            if (((CustomTimer)sender).life >= speed) //reached destination
                ((CustomTimer)sender).Stop();
        });
        timer.Start();
    }

Is there a problem with my approach that is causing this weird jerking? Any idea how to fix it?

Community
  • 1
  • 1
Austin Henley
  • 4,625
  • 13
  • 45
  • 80
  • Have you tried asynchronous operation? A System.Timers.Timer is asynchronous is by default. You'd then just have to handle the cross thread operation for the UI. But that's not difficult. – ernest Jul 29 '13 at 17:44

1 Answers1

4

CompositionTarget.Rendering will be a better fit for animating things as it fires every time a frame is about to be rendered. Try something like this instead:

    public void Shift(ScrollViewer target, double speed = 11, double distance = 20)
    {
        double startOffset = target.HorizontalOffset;
        double destinationOffset = target.HorizontalOffset + distance;

        if (destinationOffset < 0)
        {
            destinationOffset = 0;
            distance = target.HorizontalOffset;
        }

        if (destinationOffset > target.ScrollableWidth)
        {
            destinationOffset = target.ScrollableWidth;
            distance = target.ScrollableWidth - target.HorizontalOffset;
        }

        double animationTime = distance / speed;
        DateTime startTime = DateTime.Now;

        EventHandler renderHandler = null;

        renderHandler = (sender, args) =>
        {
            double elapsed = (DateTime.Now - startTime).TotalSeconds;

            if (elapsed >= animationTime)
            {
                target.ScrollToHorizontalOffset(destinationOffset);
                CompositionTarget.Rendering -= renderHandler;
            }

            target.ScrollToHorizontalOffset(startOffset + (elapsed * speed));
        };

        CompositionTarget.Rendering += renderHandler;
    }

EDIT: added range checking

Use negative distance values to scroll left.

EDIT 2:

You might want to use this CompositionTargetEx implementation instead of CompositionTarget, as it will only fire when a new frame will actually be drawn by the render thread:

https://stackoverflow.com/a/16334423/612510

EDIT 3:

Since you are on WPF (and not Silverlight, like I am more accustomed to) you might use the Stopwatch class to measure elapsed seconds instead of my DateTime.Now method.

Community
  • 1
  • 1
Mike Marynowski
  • 3,156
  • 22
  • 32
  • That's MUCH better. Thanks for that. I was using a timer event like @Austin and it was stop-start jerky. I'm running more or less the same code now inside a CompositionTarget.Rendering handler (but compensating for the variable time-since-last-render-update) and it's silky smooth! – Jon Feb 21 '14 at 17:33
  • @Jon could you share your code? i've implemented this and its still a bit janky – Julien Aug 17 '22 at 23:40