Dimitry Ivanov

Complex Android animations - Tutorial

by DIMITRY IVANOV

Achieving complex animations on Android might be a daunting task. Especially when an arbitrary number of views are involved and/or they have specific ordering (one-after-another, grouping, delays, etc). Using built-in ValueAnimator, ObjectAnimator, ViewPropertyAnimator or AnimatorSet would result in untangled mess of callbacks and won't be extensible, easy to tweak nor maintain.

gif

This tutorial will show how to achieve complex animations in a clean and easy way in just few lines of code.

This tutorial will be using Tumbleweed library.

Tumbleweed

Tumbleweed is a small library that brings the power of tween animations for Android Views and Drawables. Allows easy interpolation between any possible values and has great support for combining them in sequences. Utilizes fluent API that is easy to tweak and pleasure to read.
https://github.com/noties/Tumbleweed

All animations can be found in the sample application (head to the Releases tab to download latest APK). It provides playback functionality and displays actual source code that was used to achieve animations

Basic

gif

Simple sequential fade in of views.

Timeline.createSequence()
        .push(Tween.to(view1, Alpha.VIEW, 1.F).target(1.F))
        .push(Tween.to(view2, Alpha.VIEW, 1.F).target(1.F))
        .push(Tween.to(view3, Alpha.VIEW, 1.F).target(1.F))
        .push(Tween.to(view4, Alpha.VIEW, 1.F).target(1.F))
        .start(ViewTweenManager.get(R.id.tween_manager, group));

First, we use one of the factory methods to start configuring sequential Timeline: Timeline.createSequence(). Then we specify what kind of tweens to want to execute (push, push, push, push!). And finally triggering execution by calling start on supplied TweenManager instance.

Timeline and Tween are 2 base types that are used to define interpolations.

Tween.to(...) method accepts:

  • a target (in our case a view, but it can be anything)
  • a tween type for supplied target (TweenType<View> here)
  • a duration in seconds (1.F == 1 second)

Here we are using one of the predefined (bundled with library) tween types: Alpha.VIEW. There are quite a few of them. But what's good it's really easy to add own implementations. Generally tween types are stateless that's why library defines then as constants. The full list can be found here.

Both Tween and Timeline require a TweenManager to operate on. Here we are using ViewTweenManager that attaches to a view's drawing cycle. Using ViewTweenManager.get(@IdRes int, View) will ensure that there is only one ViewTweenManager that is attached to the specified view.

There are also DrawableTweenManager and HandlerTweenManager implementations

In order to reduce code size let's introduce some helper methods:

@NonNull
public static TweenDef<View> fadeIn(@NonNull View view) {
    return Tween.to(view, Alpha.VIEW, 1.F).target(1.F);
}

TweenDef<> is the type that is used to configure a Tween. Think of it as a builder of a Tween. All factory methods in Tween class return TweenDef<>: to, from, set, call, mark.

@NonNull
public static ViewTweenManager tweenManager(@NonNull View view) {
    return ViewTweenManager.get(R.id.tween_manager, view);
}

So, now, we can re-write our Basic code snippet like this:

Timeline.createSequence()
        .push(fadeIn(view1))
        .push(fadeIn(view2))
        .push(fadeIn(view3))
        .push(fadeIn(view4))
        .start(tweenManager(group));

Parallel

gif

Okay, let's change our animation a bit and make all views fade in simultaneously:

Timeline.createParallel()
        .push(fadeIn(view1))
        .push(fadeIn(view2))
        .push(fadeIn(view3))
        .push(fadeIn(view4))
        .start(tweenManager(group));

We did change only one line of code and instead of Timeline.createSequence() we are calling Timeline.createParallel(). That's it.

But let's change animation duration for some views:

gif

Timeline.createParallel()
        .push(fadeIn(view1))
        .push(fadeIn(view2, 2.F))
        .push(fadeIn(view3))
        .push(fadeIn(view4, 3.F))
        .start(tweenManager(group));

Previously our helper method fadeIn didn't have a duration arument, let's change that:

@NonNull
public static TweenDef<View> fadeIn(@NonNull View view) {
    return fadeIn(view, 1.F);
}

@NonNull
public static TweenDef<View> fadeIn(@NonNull View view, float duration) {
    return Tween.to(view, Alpha.VIEW, duration).target(1.F);
}

Grouped

gif

We can use Timeline to include other Timelines so can achieve grouping behaviour:

Timeline.createSequence()
        .push(fadeIn(view1))
        .push(Timeline.createParallel()
                .push(fadeIn(view2))
                .push(fadeIn(view3)))
        .push(fadeIn(view4))
        .start(tweenManager(group));

All delay

gif

If we want postpone(delay) an interpolation, we can use the delay option for a Tween:

Timeline.createParallel()
        .push(fadeIn(view1))
        .push(fadeIn(view2).delay(.25F))
        .push(fadeIn(view3).delay(.5F))
        .push(fadeIn(view4).delay(.75F))
        .start(tweenManager(group));

Warning

Please note that all time measurements in Tumbleweed library are using seconds, so to convert to a more common Android way (of using milliseconds) these would be:

  • 0.25F -> 250L
  • 0.5F -> 500L
  • 0.75F -> 750L

All delay repeat

gif

If we want to reverse the animation after it is complete we can use repeatYoyo option:

Timeline.createParallel()
        .push(fadeIn(view1))
        .push(fadeIn(view2).delay(.25F))
        .push(fadeIn(view3).delay(.5F))
        .push(fadeIn(view4).delay(.75F))
        .repeatYoyo(1, 1.F)
        .start(tweenManager(group));

repeatYoyo accepts 2 arguments: number-of-repetitions and delay between repetitions. If you want an interpolation to run endlessly pass -1 as the first argument (number).

Tip

There is also the repeat option for a Tween and it just restarts an interpolation.

Repeat individual

gif

In previous sample we used repeatYoyo option for the whole Timeline but we can also specify individual repetitions for each of the Tweens:

Timeline.createParallel()
        .push(fadeIn(view1).repeatYoyo(2, .25F))
        .push(fadeIn(view2).repeatYoyo(2, .5F))
        .push(fadeIn(view3).repeatYoyo(2, .75F))
        .push(fadeIn(view4).repeatYoyo(2, 1.F))
        .start(tweenManager(group));

Rotation

gif

Let's create a carousel-like rotation animation. We will be rotating group View by 360 degrees clockwise and all children by 360 degrees counter-clockwise.

final float rotation = 360.F;

Timeline.createParallel()
        .push(rotate(group, rotation))
        .push(rotate(view1, -rotation))
        .push(rotate(view2, -rotation))
        .push(rotate(view3, -rotation))
        .push(rotate(view4, -rotation))
        .repeatYoyo(1, 1.F)
        .start(tweenManager(group));

Where rotate helper method:

@NonNull
public static TweenDef<View> rotate(@NonNull View view, float rotation) {
    return Tween.to(view, Rotation.I, 2.F).target(rotation);
}

Expand

gif

Let's make all views appear from the center of the parent:

Timeline.createParallel()
        .push(translate(view1))
        .push(translate(view2))
        .push(translate(view3))
        .push(translate(view4))
        .repeatYoyo(1, 2.F)
        .start(tweenManager(group));

We have added another helper method translate:

@NonNull
public static TweenDef<View> translate(@NonNull View view) {
    return Tween.to(view, Translation.XY, 1.F).target(.0F, .0F);
}

As you can see it's animating a view into it's original position (0, 0). That's why before running this interpolation we must place a view in the center of the parent:

public static void placeViewInCenterOf(@NonNull View parent, @NonNull View view) {

    final int centerX = (parent.getRight() + parent.getLeft()) / 2;
    final int centerY = (parent.getBottom() + parent.getTop()) / 2;

    final Point point = ViewUtils.relativeTo(parent, view);

    final int expectedX = centerX - (view.getWidth() / 2);
    final int expectedY = centerY - (view.getHeight() / 2);

    view.setTranslationX(expectedX - point.x);
    view.setTranslationY(expectedY - point.y);
}

And run it before starting interpolation:

placeViewInCenterOf(group, view1);
placeViewInCenterOf(group, view2);
placeViewInCenterOf(group, view3);
placeViewInCenterOf(group, view4);

To make sure that all views are measured (have width and height attributes are calculated) we will use an utility method from ru.noties.tumbleweed.android.utils.ViewUtils:

ViewUtils.whenReady(group, view -> {
    
});

So, the full code snippet will be:

ViewUtils.whenReady(group, view -> {

    placeViewInCenterOf(group, view1);
    placeViewInCenterOf(group, view2);
    placeViewInCenterOf(group, view3);
    placeViewInCenterOf(group, view4);

    Timeline.createParallel()
            .push(translate(view1))
            .push(translate(view2))
            .push(translate(view3))
            .push(translate(view4))
            .repeatYoyo(1, 2.F)
            .start(tweenManager(group));
});

Expand with easing

gif

Let's add some easing to our Expand animation:

Timeline.createParallel()
        .push(translate(view1).ease(Bounce.OUT))
        .push(translate(view2).ease(Bounce.OUT))
        .push(translate(view3).ease(Bounce.OUT))
        .push(translate(view4).ease(Bounce.OUT))
        .repeatYoyo(1, 2.F)
        .start(tweenManager(group));

Expand sequential

gif

Let's make this: one item is animating return to original position whilst next one is being faded in:

view1.setAlpha(.0F);
view2.setAlpha(.0F);
view3.setAlpha(.0F);

Timeline.createSequence()
        .push(Timeline.createParallel()
                .push(translate(view4))
                .push(fadeIn(view3)))
        .push(Timeline.createParallel()
                .push(translate(view3))
                .push(fadeIn(view2)))
        .push(Timeline.createParallel()
                .push(translate(view2))
                .push(fadeIn(view1)))
        .push(translate(view1))
        .repeatYoyo(2, 2.F)
        .start(tweenManager(group));

Color

gif

Let's interpolate colors. For simplicity we will swap colors of views.

Timeline.createSequence()
        .push(Timeline.createParallel()
                .push(color(view1, color2))
                .push(color(view2, color1)))
        .push(Timeline.createParallel()
                .push(color(view3, color4))
                .push(color(view4, color3)))
        .repeatYoyo(1, 1.F)
        .start(tweenManager(group));

Where color helper method:

@NonNull
public static TweenDef<View> color(@NonNull View view, @ColorInt int color) {
    return Tween.to(view, Argb.BACKGROUND, 1.F).target(Argb.toArray(color));
}

Warning

Argb.BACKGROUND has one limitation: target value must a float array representing a color in a specific way. Please use Argb.toArray(color) method to construct such an array.

Text

gif

The great thing about Tumbleweed is the ability to interpolate anything, it is not tight to UI widgets. So let's interpolate some text:

Timeline.createSequence()
        .push(text(text1, 10))
        .push(text(text2, 20))
        .push(text(text3, 30))
        .push(text(text4, 40))
        .repeatYoyo(1, 1.F)
        .start(tweenManager(group));

Where text helper method:

@NonNull
public static TweenDef<TextView> text(@NonNull TextView textView, int value) {
    return Tween.to(textView, TextType.I, 1.F).target(value);
}

Note the TextType.I argument. It is not bundled with the library but defined in our code, it looks like this:

public static class TextType implements TweenType<TextView> {

    // as this class is stateless we can reuse it's instance
    public static final TextType I = new TextType();

    @Override
    public int getValuesSize() {
        // number of properties we interpolate
        return 1;
    }

    @Override
    public void getValues(@NonNull TextView textView, @NonNull float[] values) {
        values[0] = Integer.parseInt(textView.getText().toString());
    }

    @Override
    public void setValues(@NonNull TextView textView, @NonNull float[] values) {
        final int value = (int) (values[0] + .5F);
        textView.setText(String.valueOf(value));
    }
}

Conclusion

Of cause this is just scratching the surface of complex Android animations. And it's primary focus is view animations. But Tumbleweed can do much-much more. For example, interpolating some properties of a Drawable. There are a lot of samples in the main repository in the sample application. So if you feel interested you can check the source code out.

drawables


< Previous Local Maven repository
Next > We have everything