Learn how to use physics-based animations in a Custom View implementation for n atural looking animations in your app.
You’ve used all the standard Android animation techniques, but you find that they sometimes just don’t give you that extra sparkle you are looking for. You’ve wondered how to get more natural looking animations and had no luck thinking about how to do it yourself. So here you are, reading this article in the hope that you will learn how to create beautiful, natural, physics-based animations in your app. 🌈
The Problem 🕵🏽♀️
The physics-based animation library is not new, but it was largely unexplored territory for me. Having always used the “standard” animation options (i.e. view.animate()
), I had never found a need to use the physics-based animations, until I started with this particular custom view animation. This animation required that we animate a view between two points, decided by the user. Using the standard ValueAnimator
, the result was not good enough for the polish that our app requires.
Here is how I previously animated the custom ColourDropperView
using the ValueAnimator
and PropertyValuesHolder
class:
private fun animateToPoint(point: Point) { val propertyX = PropertyValuesHolder.ofFloat(ColorDropperView.PROPERTY_X, dropperPoint.x, point.x) val propertyY = PropertyValuesHolder.ofFloat(ColorDropperView.PROPERTY_Y, dropperPoint.y, point.y) val animator = ValueAnimator() animator.setValues(propertyX, propertyY) animator.interpolator = OvershootInterpolator() animator.duration = 100 animator.addUpdateListener { animation -> val animatedX = animation.getAnimatedValue(ColorDropperView.PROPERTY_X) as Float val animatedY = animation.getAnimatedValue(ColorDropperView.PROPERTY_Y) as Float setPoint(Point(animatedX, animatedY)) } animator.start() }
The PropertyValuesHolder
is useful when creating custom animations on our own properties. When using it, the animated property values can be fetched from the animation in the AnimationUpdateListener
callback. At this point, the values are interpolated between the start and end values we initially provide. We can then go ahead and perform the draw
operation (by calling invalidate()
on our custom view) using these new animated values and the view will animate 🤩. In our case, the setPoint()
method calls invalidate()
and the draw()
function uses the new point values to draw itself.
Don’t get me wrong — the above animation is okay in most contexts but we would like it to look more fluid. We need to animate it elegantly between these two positions.
One of the problems with the above animation is that we needed to specify a duration that the animation should take. We specified 100ms
which moved the view at a high speed. But you may also notice that the ColorDropperView
moves a lot faster when the distance between the start and end point is larger. We could play around with the duration property until it looked more acceptable. Ideally, we want the velocity to remain the same and the animation to look consistent, no matter the distance between the two points.
The Solution: SpringAnimation ✨
In order to make the animation more fluid, we need to switch to using the SpringAnimation
class (documentation can be found here). The SpringAnimation
class allows us to set the property which we will be animating, the velocity and the end value that the property should use.
To use the SpringAnimation
class, we need to include the dependency in our build.gradle
file:
implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
There are a bunch of built-in properties that we can use to achieve some standard effects, such as SCALE_X
, ROTATION
and ALPHA
properties (check the documentation for the full list here). In our case, we needed to animate a custom property — the colour dropper’s X and Y point (the underlying data structure that the view depends on for drawing). So we need to do things a bit differently.
We need to take a look at SpringAnimation
FloatPropertyCompat
SpringAnimation
FloatPropertyCompat
objects for the X position and the Y position on screen:
private val floatPropertyAnimX = object : FloatPropertyCompat<ColorDropperView>(PROPERTY_X) { override fun setValue(dropper: ColorDropperView?, value: Float) { dropper?.setDropperX(value) } override fun getValue(dropper: ColorDropperView?): Float { return dropper?.getDropperX() ?: 0f } } private val floatPropertyAnimY = object : FloatPropertyCompat<ColorDropperView>(PROPERTY_Y) { override fun setValue(dropper: ColorDropperView?, value: Float) { dropper?.setDropperY(value) } override fun getValue(dropper: ColorDropperView?): Float { return dropper?.getDropperY() ?: 0f } }
These two objects access two custom methods on the ColorDropperView
class and the setValue
methods will be called whilst the animation is running, which will set the new interpolated values. In this case, setDropperX()
and setDropperY()
are custom methods in our ColorDropperView
class. When these methods are invoked, they change the underlying value and call invalidate()
which will trigger another redraw of the view.
Once we have our properties defined, we can then go on to implement the SpringAnimation
effect with these properties.
Now we can see, our animateToPoint()
function uses the SpringAnimation
class, passes in the reference to the ColorDropperView
(this
) and we can set a few properties on the animation (such asstiffness
and dampingRatio
). We then call start()
and the animation will run.
private fun animateToPoint(point: Point) { SpringAnimation(this, floatPropertyAnimX, point.x).apply { spring.stiffness = SpringForce.STIFFNESS_MEDIUM spring.dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY start() } SpringAnimation(this, floatPropertyAnimY, point.y).apply { spring.stiffness = SpringForce.STIFFNESS_MEDIUM spring.dampingRatio = SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY start() } }
It is worth noting, that we don’t need to (and we can’t) specify the duration of this animation, which makes total sense! In the real world, when something is falling or moving, we can only calculate how long it’ll take based on its mass, stiffness, velocity and other factors. We cannot tell the object how long it should take.
Here is a recording of how the animation works now using the SpringAnimation
class. Much smoother and more natural looking, don’t you think?
Damping Ratio (bouncy-ness) 🎾
The dampingRatio
that we can set on a SpringAnimation
determines how much bounce the animation will have. There are some built-in options for the dampingRatio
that’ll produce different results:
DAMPING_RATIO_NO_BOUNCY
DAMPING_RATIO_LOW_BOUNCY
DAMPING_RATIO_MEDIUM_BOUNCY
(default)DAMPING_RATIO_HIGH_BOUNCY
We are also able to set a value between 0 and 1 for this ratio if preferred. Here are three examples of the effect the dampingRatio
has on our custom view:
Stiffness
Another property that we can set on a SpringAnimation
is the stiffness
of the spring force. Like the dampingRatio
, we can choose from a few predefined options:
STIFFNESS_LOW
STIFFNESS_VERY_LOW
STIFFNESS_MEDIUM
(default)STIFFNESS_HIGH
The stiffness affects how long the animation will take: if the spring is very stiff (STIFFNESS_HIGH
) the animation will perform quicker than if the stiffness is low.
Below is an example of some of the different stiffness values in action:
Velocity 🚗
SpringAnimations
can also have their startVelocity
set using setStartVelocity()
. This value is specified in pixels per second. If you would like to specify it, you should convert from a dp
value into pixels to ensure the animation looks consistent across different devices. The default startVelocity
is 0
.
Here is an example of how to set the startVelocity
to 5000dp per second:
SpringAnimation(this, floatPropertyAnimY, point.y).apply { setStartVelocity(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5000f, resources.displayMetrics)) start() }
This is what different start velocities look like on the custom view:
Cancel SpringAnimation ✋🏾
Another great part about physics-based animations is that they can be cancelled midway through the animation if required. Calling SpringAnimation#cancel()
will terminate the animation. There is also the option toSpringAnimation#skipToEnd()
which will immediately show the end state of the animation (this can cause a visual jump — as if the animation wasn’t implied).
Dynamic Animation — KTX Functions
There are currently some extension functions provided by the following KTX dependency (check for the latest version here):
implementation "androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha01"
The alpha version was released on the 9th February 2019, but there are some new changes that haven’t been released yet, which will clean up this code quite a bit. Take a look here at the new extension functions that will be provided soon. The clean up of creating the FloatPropertyCompat
objects is particularly interesting and will help clean up this code in the future.
Finally 🧚🏼♀️
The physics-based animations in Android are great alternatives when you have animations that need to look more natural. They are also great for when you are moving things around on the screen and you aren’t sure how long the animation should take. Rather don’t try to guess those duration values, use physics animations instead. 👩🏻🔬
For our animation, we ended up going with the DAMPING_RATIO_MEDIUM_BOUNCY
,STIFFNESS_MEDIUM
and the startVelocity
set at 0
. This was the final animation that we stuck with:
Where else have you found physics-based animations to be useful? Let me know your thoughts on Twitter! 🌈
Thanks to Josh Leibstein, Garima Jain, Nick Rout and Dan Galasko for reviewing this post. 💚