springanimation position 60fpsspringanimation rotation 60fpsspringanimation scale 60fps

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:

  • 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.

      springanimation dampingratio

      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.

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!

trampoline dog

Examples

Setup

To get started, add the following dependency to your module’s build.gradle:

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.

  1. Create a SpringAnimation object for your View with a specified ViewProperty
  2. Create a SpringForce object and set your desired parameters (which are described above).
  3. Apply the created SpringForce to your SpringAnimation.
  4. Start the animation.
// 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:

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:

  1. Drag the view.
  2. Move it around.
  3. Release it.
  4. The view springs back to its original position.
SpringAnimation Position GIF
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:

  1. Grab the view.
  2. Spin it.
  3. Release it.
  4. The view spins back to its original position, again with a bounce.
SpringAnimation Rotation GIF
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:

  1. Grab it with 2 fingers.
  2. Do a typical pinching gesture to zoom in or out.
  3. Release it.
  4. The view scales back to its original size.
SpringAnimation Scale GIF
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!