Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatically scrolling a ScrollViewer with a timer becomes jerky

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?

like image 326
Austin Henley Avatar asked Oct 23 '25 19:10

Austin Henley


1 Answers

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.

like image 182
Mike Marynowski Avatar answered Oct 26 '25 09:10

Mike Marynowski