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.

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

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)

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

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:

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

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

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

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

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

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

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

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

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

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

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 > { public static final TextType I = new TextType ( ) ; @Override public int getValuesSize ( ) { 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.