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.
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
andTween
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
andHandlerTweenManager
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 aTween
. Think of it as a builder of aTween
. All factory methods inTween
class returnTweenDef<>
: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 aTween
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 useArgb.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> {
// 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.