Android Spring Animation – Physics Based Animation

Filed Under: Android

In this tutorial, we’ll implement Spring based animations that are a part of the support library in our android application. Spring Animation is part of Android Physics Based animation API.

Android Spring Animation

Android Spring Animation animates views based on spring properties: dampness, stiffness, bouncy.

Spring Animation can be implemented in your project once you add the following dependency in your dependencies section in the build.gradle:


dependencies {
      implementation 'com.android.support:support-dynamic-animation:27.1.1'
  }

To create a Spring Animation, we need to create a SpringAnimation class and pass along the animation type – translation/rotation/scaling along with the final position of the view and velocity of the animation.

We can also set additional properties i.e. Stiffness and Damping.

A spring in real life bounces when it returns to its final position.

The Higher the Damping and lower the stiffness the more it would oscillate/bounce.

To animate a view the final position of the View must always be assigned before.

To start the animation we invoke either start() or animateToFinalPosition(Float finalPosition). The latter updates the final position and calls start() internally.

Besides, updateListener and removeListener are the respective listeners for listening to updates and removing the listener when it’s no longer needed.

Simple Spring Animation

In java, to set spring animation on any view we do:


SpringAnimation springAnim = new SpringAnimation(fab, SpringAnimation.TRANSLATION_Y);
SpringForce springForce = new SpringForce();
springForce.setFinalPosition(-200f);
springForce.setStiffness(SpringForce.STIFFNESS_LOW);
springForce.setDampingRatio(SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
springAnim.setSpring(springForce);
springAnim.start();

The position must be a floating value. A negative in the Y direction is upwards. A negative in the X direction is leftwards. fab is the view instance over which the animation happens.

Dragging View Spring Like Movement

We can also drag a certain view and see it bounce like a spring. For this, we need to set the touch listener on the view.


private SpringAnimation xAnimation;
private SpringAnimation yAnimation;
ImageView imageView;

private void imageViewDragSpringAnimation() {

        imageView.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
        imageView.setOnTouchListener(touchListener);
    }

    private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            xAnimation = createSpringAnimation(imageView, SpringAnimation.X, imageView.getX(),
                    SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
            yAnimation = createSpringAnimation(imageView, SpringAnimation.Y, imageView.getY(),
                    SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
        }
    };

    private View.OnTouchListener touchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    dX = v.getX() - event.getRawX();
                    dY = v.getY() - event.getRawY();
                    // cancel animations
                    xAnimation.cancel();
                    yAnimation.cancel();
                    break;
                case MotionEvent.ACTION_MOVE:
                    imageView.animate()
                            .x(event.getRawX() + dX)
                            .y(event.getRawY() + dY)
                            .setDuration(0)
                            .start();
                    break;
                case MotionEvent.ACTION_UP:
                    xAnimation.start();
                    yAnimation.start();
                    break;
            }
            return true;
        }
    };

    public SpringAnimation createSpringAnimation(View view,
                                                 DynamicAnimation.ViewProperty property,
                                                 float finalPosition,
                                                 float stiffness,
                                                 float dampingRatio) {
        SpringAnimation animation = new SpringAnimation(view, property);
        SpringForce springForce = new SpringForce(finalPosition);
        springForce.setStiffness(stiffness);
        springForce.setDampingRatio(dampingRatio);
        animation.setSpring(springForce);
        return animation;
    }

The GlobalLayoutListener is triggered when the ImageView is displayed on the screen with its width and height finalized.

Once that is done, we set the final position of the SpringAnimation to the current X and Y coordinates of the ImageView at rest.

onTouchListener is used to drag the view on the screen.

When the drag starts we capture the difference between view’s top left corner and touch point and on moving the view, the difference is added to the current position.

When the drag stops the SpringAnimation is canceled and the view returns to its original position.

Chained Spring Animation

In this type of animation, we group together views to make them animate together.

Each would animate differently depending on its Spring Properties.

In the following section, we’ll implement each of the above types of Spring Animations in our Android Studio Project.

Project Structure

android spring animation project structure

Android Spring Animation Code

The code for the activity_main.xml layout is given below:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/AppTheme.PopupOverlay" />

    </android.support.design.widget.AppBarLayout>

    <include layout="@layout/content_main" />


</android.support.design.widget.CoordinatorLayout>

The code for the content_main.xml layout is given below:


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">


    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        android:layout_marginBottom="16dp"
        android:layout_marginStart="16dp"
        app:backgroundTint="@color/colorPrimary"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        android:layout_marginBottom="16dp"
        android:layout_marginEnd="16dp"
        app:backgroundTint="@android:color/black"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />


    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:layout_marginBottom="16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />


    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        app:backgroundTint="@android:color/holo_green_dark"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        app:backgroundTint="#1A1A1A"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.4"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/fab4" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab6"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/fab_margin"
        app:backgroundTint="@android:color/holo_red_dark"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/fab5" />

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:background="#FF3456"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

We’ve created six FloatingActionButton. Three for simple Spring Animation and three for the chained spring animations.

The sole ImageView would be used to show how dragging the view causes the Spring Animation.

The code for the MainActivity.java is as follows:


package com.journaldev.androidspringanimations;

import android.os.Bundle;
import android.support.animation.DynamicAnimation;
import android.support.animation.SpringAnimation;
import android.support.animation.SpringForce;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MotionEvent;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

public class MainActivity extends AppCompatActivity {

    private SpringAnimation xAnimation;
    private SpringAnimation yAnimation;
    ImageView imageView;

    private float dX;
    private float dY;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        imageViewDragSpringAnimation();
        chainedSpringAnimation();

        final FloatingActionButton fab = findViewById(R.id.fab);
        final FloatingActionButton fab2 = findViewById(R.id.fab2);
        final FloatingActionButton fab3 = findViewById(R.id.fab3);

        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                SpringAnimation springAnim = new SpringAnimation(fab, SpringAnimation.TRANSLATION_Y);
                SpringForce springForce = new SpringForce();
                springForce.setFinalPosition(-200f);
                springForce.setStiffness(SpringForce.STIFFNESS_LOW);
                springForce.setDampingRatio(SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
                springAnim.setSpring(springForce);
                springAnim.start();
            }
        });

        fab2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                final SpringAnimation springAnim = new SpringAnimation(fab2, SpringAnimation.TRANSLATION_Y);
                SpringForce springForce = new SpringForce();
                springForce.setFinalPosition(-200f);
                springForce.setStiffness(SpringForce.STIFFNESS_HIGH);
                springForce.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
                springAnim.setSpring(springForce);
                springAnim.start();
            }
        });

        fab3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                final SpringAnimation springAnim = new SpringAnimation(fab3, SpringAnimation.TRANSLATION_X);
                SpringForce springForce = new SpringForce();
                springForce.setFinalPosition(-200f);
                springForce.setStiffness(SpringForce.STIFFNESS_MEDIUM);
                springForce.setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY);
                springAnim.setSpring(springForce);
                springAnim.start();
            }
        });


    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void imageViewDragSpringAnimation() {

        imageView = findViewById(R.id.imageView);
        imageView.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
        imageView.setOnTouchListener(touchListener);
    }

    private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            xAnimation = createSpringAnimation(imageView, SpringAnimation.X, imageView.getX(),
                    SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
            yAnimation = createSpringAnimation(imageView, SpringAnimation.Y, imageView.getY(),
                    SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
        }
    };

    private View.OnTouchListener touchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    // capture the difference between view's top left corner and touch point
                    dX = v.getX() - event.getRawX();
                    dY = v.getY() - event.getRawY();
                    // cancel animations
                    xAnimation.cancel();
                    yAnimation.cancel();
                    break;
                case MotionEvent.ACTION_MOVE:
                    //  a different approach would be to change the view's LayoutParams.
                    imageView.animate()
                            .x(event.getRawX() + dX)
                            .y(event.getRawY() + dY)
                            .setDuration(0)
                            .start();
                    break;
                case MotionEvent.ACTION_UP:
                    xAnimation.start();
                    yAnimation.start();
                    break;
            }
            return true;
        }
    };

    public SpringAnimation createSpringAnimation(View view,
                                                 DynamicAnimation.ViewProperty property,
                                                 float finalPosition,
                                                 float stiffness,
                                                 float dampingRatio) {
        SpringAnimation animation = new SpringAnimation(view, property);
        SpringForce springForce = new SpringForce(finalPosition);
        springForce.setStiffness(stiffness);
        springForce.setDampingRatio(dampingRatio);
        animation.setSpring(springForce);
        return animation;
    }


    public SpringAnimation createSpringAnimation(View view,
                                                 DynamicAnimation.ViewProperty property,
                                                 float stiffness,
                                                 float dampingRatio) {
        SpringAnimation animation = new SpringAnimation(view, property);
        SpringForce springForce = new SpringForce();
        springForce.setStiffness(stiffness);
        springForce.setDampingRatio(dampingRatio);
        animation.setSpring(springForce);
        return animation;
    }


    private void chainedSpringAnimation() {

        final FloatingActionButton fab4 = findViewById(R.id.fab4);
        final FloatingActionButton fab5 = findViewById(R.id.fab5);
        final FloatingActionButton fab6 = findViewById(R.id.fab6);

        final SpringAnimation firstXAnim = createSpringAnimation(fab5, DynamicAnimation.X, SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
        final SpringAnimation firstYAnim = createSpringAnimation(fab5, DynamicAnimation.Y, SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
        final SpringAnimation secondXAnim = createSpringAnimation(fab6, DynamicAnimation.X, SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);
        final SpringAnimation secondYAnim = createSpringAnimation(fab6, DynamicAnimation.Y, SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_HIGH_BOUNCY);


        final ViewGroup.MarginLayoutParams fab5Params = (ViewGroup.MarginLayoutParams) fab5.getLayoutParams();
        final ViewGroup.MarginLayoutParams fab6Params = (ViewGroup.MarginLayoutParams) fab6.getLayoutParams();


        firstXAnim.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {
            @Override
            public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float v, float v1) {

                secondXAnim.animateToFinalPosition(v + ((fab5.getWidth() -
                        fab6.getWidth()) / 2));

            }
        });

        firstYAnim.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {
            @Override
            public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float v, float v1) {
                secondYAnim.animateToFinalPosition(v + fab5.getHeight() +
                        fab6Params.topMargin);
            }
        });


        fab4.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                switch (motionEvent.getActionMasked()) {
                    case MotionEvent.ACTION_DOWN:
                        dX = view.getX() - motionEvent.getRawX();
                        dY = view.getY() - motionEvent.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:

                        float newX = motionEvent.getRawX() + dX;
                        float newY = motionEvent.getRawY() + dY;

                        view.animate().x(newX).y(newY).setDuration(0).start();
                        firstXAnim.animateToFinalPosition(newX + ((fab4.getWidth() -
                                fab5.getWidth()) / 2));
                        firstYAnim.animateToFinalPosition(newY + fab4.getHeight() +
                                fab5Params.topMargin);

                        break;
                }
                return true;
            }
        });

    }
}

The first type of Simple Spring Animation is performed on fab, fab2, and fab3. One of them would translate horizontally.

imageViewDragSpringAnimation() triggers the second type of animation as we had discussed before.

chainedSpringAnimation() method triggers the third type.

In the chainedSpringAnimation we drag the first fab i.e. fab4 which would trigger animation update listeners of the X and Y Spring Animation. In each of these listeners, we trigger the Spring Animations for the other two Floating Action Button thus chaining them.

The output of the above application in action is given below:

android spring animation, android physics based animation example

This brings an end to this tutorial. You can download the project from the link below:

Reference: API Doc

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages