You might say “whatever, I’m just gonna slap a BounceInterpolator or an OvershootInterpolatoron 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:

// 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

classRotationActivity:AppCompatActivity(){

privatecompanionobjectParams{

val INITIAL_ROTATION=0f

val STIFFNESS=SpringForce.STIFFNESS_MEDIUM

val DAMPING_RATIO=SpringForce.DAMPING_RATIO_HIGH_BOUNCY

}

lateinit varrotationAnimation: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)

varpreviousRotation=0f

varcurrentRotation=0f

rotatingView.setOnTouchListener{view,event->

val centerX=view.width/2.0

val centerY=view.height/2.0

valx=event.x

valy=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

classScaleActivity:AppCompatActivity(){

privatecompanionobjectParams{

val INITIAL_SCALE=1f

val STIFFNESS=SpringForce.STIFFNESS_MEDIUM

val DAMPING_RATIO=SpringForce.DAMPING_RATIO_HIGH_BOUNCY

}

lateinit varscaleXAnimation:SpringAnimation

lateinit varscaleYAnimation:SpringAnimation

lateinit varscaleGestureDetector: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

}

}

privatefun setupPinchToZoom(){

varscaleFactor=1f

scaleGestureDetector=ScaleGestureDetector(this,

object:ScaleGestureDetector.SimpleOnScaleGestureListener(){

override fun onScale(detector:ScaleGestureDetector):Boolean{

scaleFactor *=detector.scaleFactor

scalingView.scaleX *=scaleFactor

scalingView.scaleY *=scaleFactor

returntrue

}

})

}

}

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!