Animation / ViewPager / ViewPagerAnimator

ViewPagerAnimator – The Internals

Earlier this month I released a new lightweight, yet extremely powerful ViewPager animation library named, unsurprisingly ViewPagerAnimator. ViewPagerAnimator is designed to animate arbitrary values as the user navigates between pages within a ViewPager, and will precisely follow the motion of h[is|er] finger. There is nothing groundbreaking in terms of the techniques I’m using to do this – I previously did something very similar in Prism. However, there are some very nice subtleties in terms of the API design.


In this final article about ViewPagerAnimator we’ll take a look inside and explain how it actually works. The main thing that enables us to easily hook in to the operation of the ViewPager itself is by implementing the ViewPager.OnPageChangeListener interface which will provide us with callback as the user swipes or switches between view. Before we dive in to that let’s first look at those strongly-typed factory methods:

public class ViewPagerAnimator<V> implements ViewPager.OnPageChangeListener {

    private final Provider<V> provider;
    private final Property<V> property;
    private final TypeEvaluator<V> evaluator;

    private ViewPager viewPager;

    private V startValue;
    private V endValue;

    private Interpolator interpolator;

    private int currentPage = 0;
    private int targetPage = -1;
    private int lastPosition = 0;

    public static ViewPagerAnimator<Integer> ofInt(ViewPager viewPager, Provider<Integer> provider, Property<Integer> property) {
        final IntEvaluator evaluator = new IntEvaluator();
        final Interpolator interpolator = new LinearInterpolator();

        return ofInt(viewPager, provider, property, evaluator, interpolator);
    }

    @VisibleForTesting
    static ViewPagerAnimator<Integer> ofInt(ViewPager viewPager,
                                            Provider<Integer> provider,
                                            Property<Integer> property,
                                            TypeEvaluator<Integer> evaluator,
                                            Interpolator interpolator) {
        return new ViewPagerAnimator<>(viewPager, provider, property, evaluator, interpolator);
    }

    public static ViewPagerAnimator<Integer> ofArgb(ViewPager viewPager, Provider<Integer> provider, Property<Integer> property) {
        @SuppressWarnings("unchecked") final TypeEvaluator<Integer> evaluator = new ArgbEvaluator();
        final Interpolator interpolator = new LinearInterpolator();

        return ofArgb(viewPager, provider, property, evaluator, interpolator);
    }

    @VisibleForTesting
    static ViewPagerAnimator<Integer> ofArgb(ViewPager viewPager,
                                             Provider<Integer> provider,
                                             Property<Integer> property,
                                             TypeEvaluator<Integer> evaluator,
                                             Interpolator interpolator) {
        return new ViewPagerAnimator<>(viewPager, provider, property, evaluator, interpolator);
    }

    public static ViewPagerAnimator<Number> ofFloat(ViewPager viewPager, Provider<Number> provider, Property<Number> property) {
        final FloatEvaluator evaluator = new FloatEvaluator();
        final Interpolator interpolator = new LinearInterpolator();

        return ofFloat(viewPager, provider, property, evaluator, interpolator);
    }

    @VisibleForTesting
    static ViewPagerAnimator<Number> ofFloat(ViewPager viewPager,
                                             Provider<Number> provider,
                                             Property<Number> property,
                                             TypeEvaluator<Number> evaluator,
                                             Interpolator interpolator) {
        return new ViewPagerAnimator<>(viewPager, provider, property, evaluator, interpolator);
    }

    public ViewPagerAnimator(@NonNull ViewPager viewPager,
                             @NonNull Provider<V> provider,
                             @NonNull Property<V> property,
                             @NonNull TypeEvaluator<V> evaluator) {
        this(viewPager, provider, property, evaluator, DEFAULT_INTERPOLATOR);
    }

    public ViewPagerAnimator(@NonNull ViewPager viewPager,
                             @NonNull Provider<V> provider,
                             @NonNull Property<V> property,
                             @NonNull TypeEvaluator<V> evaluator,
                             @NonNull Interpolator interpolator) {
        setViewPager(viewPager);
        this.provider = provider;
        this.property = property;
        this.evaluator = evaluator;
        setInterpolator(interpolator);
    }
    .
    .
    .
    public void destroy() {
        viewPager.removeOnPageChangeListener(this);
    }

    public void setViewPager(ViewPager viewPager) {
        if (this.viewPager != null) {
            this.viewPager.removeOnPageChangeListener(this);
        }
        this.viewPager = viewPager;
        this.viewPager.addOnPageChangeListener(this);
    }

    public void setInterpolator(Interpolator newInterpolator) {
        if (newInterpolator == null) {
            interpolator = new LinearInterpolator();
        } else {
            interpolator = newInterpolator;
        }
    }
}

Although there looks to be a lot of code here, it’s actually much simpler that it first appears. There are 6 factory methods, but only 3 of them are designed for external consumption, the others are package private and allow us to inject mocks much easier in our tests. So each of these will create a suitable TypeEvaluator and instantiate a ViewPagerAnimator.

There are the two constructors listed there as well. These can both be called externally for creating a custom type animators, as we saw previously.

Finally there are a couple of lifecycle methods which will attach and detach from a ViewPager, and finally a method to allow the override of an Interpolator.

The main workhorse of ViewPagerAnimator is the implementation of ViewPager.OnPageChangeListener:

@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    if (position == 0 && positionOffsetPixels == 0 && !isAnimating()) {
        onPageSelected(0);
    }
    if (isAnimating() && lastPosition != position || positionOffsetPixels == 0) {
        endAnimation(position);
    }
    if (positionOffsetPixels > 0) {
        beginAnimation(position);
    }
    if (isAnimating()) {
        float fraction = interpolator.getInterpolation(positionOffset);
        V value = evaluator.evaluate(fraction, startValue, endValue);
        property.set(value);
    }
    lastPosition = position;
}

private boolean isAnimating() {
    return targetPage >= 0;
}

private void endAnimation(int position) {
    currentPage = position;
    targetPage = -1;
}

private void beginAnimation(int position) {
    PagerAdapter adapter = viewPager.getAdapter();
    if (position == currentPage && position + 1 <= adapter.getCount()) {
        targetPage = position + 1;
        startValue = provider.get(position);
        endValue = provider.get(targetPage);
    } else if (position >= 0) {
        targetPage = position;
        startValue = provider.get(position);
        endValue = provider.get(currentPage);
    }
}

@Override
public void onPageSelected(int position) {
    V value = provider.get(position);
    property.set(value);
}

@Override
public void onPageScrollStateChanged(int state) {
}

The onPageScrolled() callback will be called for every tiny movement during the scroll and is where we actually calculate and apply the animated value to the Property. However, there are a number of distinct things going on. The first conditional is an initialiser to ensure that we set up the initial page state as the ViewPager is initialised. We need to handle this here because onPageScrolled() will be called when we first attach to the ViewPager and as we are responsible for setting the Property, we must set an initial value.

The next conditional checks whether the an in-progress animation should be ended because we’ve reached a stable page boundary.

Next we determine whether we should begin an animation.

Finally if, after all that, an animation is currently in-progress then we calculate a value based upon the interpolated fractional distance between pages, and apply that to the Property.

The remaining methods are a little more straightforward. Firstly isAnimating() will return whether an animation is currently in progress; endAnimation() will set the animation to a quiescent state; and beginAnimation() will, as it’s name suggest start an animation.There are two branches here which cover whether the pager transition is from left-to-right or right-to-left. Both cases initialise the start andEnd values by performing look-ups from the Provider. It is these values that are used in onPageScrolled() to work out the fractional value which gets applied to the Property.

The final two methods of ViewPager.OnPageChangeListener are onPageScrollStateChanged() which we ignore, and onPageSelected() which is called if the user selects a page by tapping on a tab in the TabBar. In this case we just set the property based on the value of the selected position – we don’t need to calculate a fractional position because we know that we are now settled on a page stable page position.

There’s nothing really that clever going on here but, as mentioned previously, it is the single method Provider & Property interfaces which facilitate the use of Java 8 method references which turn the API in to an extremely concise, fluent, and powerful one when we use Java 8.

ViewPagerAnimator is available now on jcenter compile 'com.stylingandroid.viewpageranimator:viewpageranimator:1.0.1' and the source is available here.

© 2017, Mark Allison. All rights reserved.

Copyright © 2017 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.