Introduction to SpringAnimation with examples
Have you ever wanted to do a bouncy animation like one of these on Android? If you have, you’re in for a treat!
Dynamic-animation is a new module introduced in revision 25.3.0 of the Android Support Library. It provides a small set of classes for making realistic physics-based view animations.
You might say “whatever, I’m just gonna slap a BounceInterpolator or an OvershootInterpolator on my animation and be good”. Well, in reality these two often don’t look that great. Of course, you could always write your own interpolator or implement a whole custom animation – but now there’s a much easier way.
Classes
At the time of writing this post, the module contains just 4 classes. Let’s take a look at their docs descriptions:
- DynamicAnimation<T extends DynamicAnimation<T>>
This class is the base class of physics-based animations.
- DynamicAnimation.ViewProperty
ViewProperty holds the access of a property of a View.
You probably remember those from typical animations:
ALPHA, ROTATION, ROTATION_X, ROTATION_Y, SCALE_X, SCALE_Y, SCROLL_X, SCROLL_Y, TRANSLATION_X, TRANSLATION_Y, TRANSLATION_Z, X, Y, Z
- SpringAnimation
SpringAnimation is an animation that is driven by a SpringForce.
- SpringForce
Spring Force defines the characteristics of the spring being used in the animation.
It has 3 key parameters:
- finalPosition
The spring’s rest position (or angle/scale). - stiffness
Docs say:Stiffness corresponds to the spring constant. The stiffer the spring is, the harder it is to stretch it, the faster it undergoes dampening.
The higher the stiffness, the faster the object will settle.
- dampingRatio
Docs say:Spring damping ratio describes how oscillations in a system decay after a disturbance.
Depending on the value, our object will:
1. Oscillate forever around the rest position.
2. Oscillate around its rest position until it settles.
3. Stop gently, as soon as possible.
4. Stop quickly and without overshoot.
SpringForce has 4 predefined float constants for both stiffness and dampingRatio, but it’s also possible to set custom values.
- finalPosition
As you can see the package is currently quite small. If you’re looking for something a bit more complex in terms of spring dynamics, take a look at Facebook’s Rebound library.
Note
DynamicAnimation doesn’t extend Animation, so you won’t be able to just replace one or use it in an AnimationSet. Don’t worry though, the whole thing is still very simple.
Boooring, let’s go already!
Examples
GitHub repository: android-springanimation-examples
Setup
To get started, add the following dependency to your module’s build.gradle:
1 2 3 | dependencies { compile 'com.android.support:support-dynamic-animation:25.3.0' } |
The following code is written in Kotlin (give it a try if you haven’t yet!).
Making a SpringAnimation
Well, it won’t really be generic in a programming sense, but let’s start with how every SpringAnimation is made.
- Create a SpringAnimation object for your View with a specified ViewProperty
- Create a SpringForce object and set your desired parameters (which are described above).
- Apply the created SpringForce to your SpringAnimation.
- Start the animation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // create an animation for your view and set the property you want to animate val animation = SpringAnimation(v = view, property = SpringAnimation.X) // create a spring with desired parameters val spring = SpringForce() spring.finalPosition = 100f // can also be passed directly in the constructor spring.stiffness = SpringForce.STIFFNESS_LOW // optional, default is STIFFNESS_MEDIUM spring.dampingRatio = SpringForce.DAMPING_RATIO_HIGH_BOUNCY // optional, default is DAMPING_RATIO_MEDIUM_BOUNCY // set your animation's spring animation.spring = spring // animate! animation.start() |
I moved the creation code into a simple utility function to make the code in these examples a bit more readable:
1 2 3 4 5 6 7 8 9 10 11 12 | fun createSpringAnimation(view: View, property: DynamicAnimation.ViewProperty, finalPosition: Float, stiffness: Float, dampingRatio: Float): SpringAnimation { val animation = SpringAnimation(view, property) val spring = SpringForce(finalPosition) spring.stiffness = stiffness spring.dampingRatio = dampingRatio animation.spring = spring return animation } |
Example #1 – Position
Let’s say we have an arbitrary view positioned in the center of the screen
We want to achieve the following behavior:
- Drag the view.
- Move it around.
- Release it.
- The view springs back to its original position.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | class PositionActivity : AppCompatActivity() { private companion object Params { val STIFFNESS = SpringForce.STIFFNESS_MEDIUM val DAMPING_RATIO = SpringForce.DAMPING_RATIO_HIGH_BOUNCY } lateinit var xAnimation: SpringAnimation lateinit var yAnimation: SpringAnimation override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_position) // create X and Y animations for view's initial position once it's known movingView.viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { xAnimation = createSpringAnimation( movingView, SpringAnimation.X, movingView.x, STIFFNESS, DAMPING_RATIO) yAnimation = createSpringAnimation( movingView, SpringAnimation.Y, movingView.y, STIFFNESS, DAMPING_RATIO) movingView.viewTreeObserver.removeOnGlobalLayoutListener(this) } }) var dX = 0f var dY = 0f movingView.setOnTouchListener { view, event -> when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { // capture the difference between view's top left corner and touch point dX = view.x - event.rawX dY = view.y - event.rawY // cancel animations so we can grab the view during previous animation xAnimation.cancel() yAnimation.cancel() } MotionEvent.ACTION_MOVE -> { // a different approach would be to change the view's LayoutParams. movingView.animate() .x(event.rawX + dX) .y(event.rawY + dY) .setDuration(0) .start() } MotionEvent.ACTION_UP -> { xAnimation.start() yAnimation.start() } } true } } } |
Example #2 – Rotation
There’s a rotating view on our screen which behaves like this:
- Grab the view.
- Spin it.
- Release it.
- The view spins back to its original position, again with a bounce.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | class RotationActivity : AppCompatActivity() { private companion object Params { val INITIAL_ROTATION = 0f val STIFFNESS = SpringForce.STIFFNESS_MEDIUM val DAMPING_RATIO = SpringForce.DAMPING_RATIO_HIGH_BOUNCY } lateinit var rotationAnimation: SpringAnimation override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_rotation) // create a rotation SpringAnimation rotationAnimation = createSpringAnimation( rotatingView, SpringAnimation.ROTATION, INITIAL_ROTATION, STIFFNESS, DAMPING_RATIO) var previousRotation = 0f var currentRotation = 0f rotatingView.setOnTouchListener { view, event -> val centerX = view.width / 2.0 val centerY = view.height / 2.0 val x = event.x val y = event.y // angle calculation fun updateCurrentRotation() { currentRotation = view.rotation + Math.toDegrees(Math.atan2(x - centerX, centerY - y)).toFloat() } when (event.actionMasked) { MotionEvent.ACTION_DOWN -> { // cancel so we can grab the view during previous animation rotationAnimation.cancel() updateCurrentRotation() } MotionEvent.ACTION_MOVE -> { // save current rotation previousRotation = currentRotation updateCurrentRotation() // rotate view by angle difference val angle = currentRotation - previousRotation view.rotation += angle } MotionEvent.ACTION_UP -> rotationAnimation.start() } true } } } |
Example #3 – Scale
As usual, there’s a view on our screen (it could be a photo) which has the following behavior:
- Grab it with 2 fingers.
- Do a typical pinching gesture to zoom in or out.
- Release it.
- The view scales back to its original size.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | class ScaleActivity : AppCompatActivity() { private companion object Params { val INITIAL_SCALE = 1f val STIFFNESS = SpringForce.STIFFNESS_MEDIUM val DAMPING_RATIO = SpringForce.DAMPING_RATIO_HIGH_BOUNCY } lateinit var scaleXAnimation: SpringAnimation lateinit var scaleYAnimation: SpringAnimation lateinit var scaleGestureDetector: ScaleGestureDetector override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_scale) // create scaleX and scaleY animations scaleXAnimation = createSpringAnimation( scalingView, SpringAnimation.SCALE_X, INITIAL_SCALE, STIFFNESS, DAMPING_RATIO) scaleYAnimation = createSpringAnimation( scalingView, SpringAnimation.SCALE_Y, INITIAL_SCALE, STIFFNESS, DAMPING_RATIO) setupPinchToZoom() scalingView.setOnTouchListener { _, event -> if (event.action == MotionEvent.ACTION_UP) { scaleXAnimation.start() scaleYAnimation.start() } else { // cancel animations so we can grab the view during previous animation scaleXAnimation.cancel() scaleYAnimation.cancel() // pass touch event to ScaleGestureDetector scaleGestureDetector.onTouchEvent(event) } true } } private fun setupPinchToZoom() { var scaleFactor = 1f scaleGestureDetector = ScaleGestureDetector(this, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { scaleFactor *= detector.scaleFactor scalingView.scaleX *= scaleFactor scalingView.scaleY *= scaleFactor return true } }) } } |
Note
The view’s scale value can go below 0 during the animation (i.e. if you scale it up too much before releasing).
If you look closely at the above animation, you’ll see that it flips the Android upside down for a split second. ?
Wrap-up
SpringAnimation makes it quite easy to implement some basic dynamic animations. It’s a nice option, as a little bounciness can help break that linear monotony of a generic Material application. But as with any animations – be careful not to overuse them or you might drive your users crazy!
Build a mobile app with experts with 12 years of experience
Together, we will get your project off the ground and achieve amazing results
Great examples!
BTW, in example #3, you can use setMinValue(float) on the spring animation to prevent the scale from going negative.
Hey, thanks for the comment.
That’s a good idea! Though I still think it could be limited by the library itself, as I can’t imagine a situation where someone would want a glitchy animation like this.