Meaningful motion for Activities transition in Android Lollipop

    As we can see at Material Design specs:
Motion in the world of material design is used to describe spatial relationships, functionality, and intention with beauty and fluidity. Motion design can effectively guide the user’s attention in ways that both inform and delight. Use motion to smoothly transport users between navigational contexts, explain changes in the arrangement of elements on a screen, and reinforce element hierarchy.
    From API 21, Material Design has bring us the new way to switch Activity (activity transition) with animations and of course, the element located on these screen are also affected by this transition process.
    We must apply those animations carefully to avoid the app become a true Pixar animation movie. In this post, I will present some customizing of Material meaningful motion, make our application look smoothly.
    DEMO VIDEO:

Prerequisites

    In order to custom activity transition and other related animations, make sure that your min-sdk of your project is 21 or higher. I also add some necessary dependencies which use for my sample project later:
app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.2"

    defaultConfig {
        applicationId "info.devexchanges.uimotion"
        minSdkVersion 21
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:24.2.0'
    compile 'com.android.support:design:24.2.0'
    compile 'com.android.support:gridlayout-v7:24.2.0'
    compile 'com.android.support:cardview-v7:24.2.0'
}

Custom Activity Transitions in xml

    Before Android Lollipop (API Level 21) we could only customize the activity transition animation over entire activity. All views were animated together. But now we can specify how each view is animated during that transition.
    Suppose I have 3 activities: the first called MainActivity which display 3 pictures and after click at any one, app will redirect user to second activity to show selected picture descriptions (called DetailsActivity). This activity contains a FloatingActionButton, when click on it, a translucent activity (SharingActivity) appears which give some options to shared the content. It has a yellow shape which is a drawable defined as image source of a ImageView. Now, we'll custom some animations based on xml resources.
    Create a folder named transaction in res directory, all animations xml files will put here.
    We can specify custom animations for enter and exit transitions and for transitions of shared elements between activities.
  • An enter transition determines how views move into the initial scene of the started activity. 
  • An exit transition determines how views move out of the scene when starting a new activity. 
  • A shared elements transition determines how views are shared between two activities transition.
These are all xml files that defining animation for transacting from MainActivity to DetailsActivity: and showing translucent SharingActivity:
main_reenter.xml
<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:slideEdge="top">
    <targets>
        <target android:excludeId="@android:id/statusBarBackground" />
        <target android:excludeId="@android:id/navigationBarBackground" />
    </targets>
</slide>
main_exit.xml
<?xml version="1.0" encoding="utf-8"?>
<explode xmlns:android="http://schemas.android.com/apk/res/android">
    <targets>
        <target android:excludeId="@android:id/statusBarBackground" />
        <target android:excludeId="@android:id/navigationBarBackground" />
    </targets>
</explode>
detail_enter.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
               android:transitionOrdering="together">

    <slide
        android:slideEdge="bottom">
        <targets>
            <target android:targetId="@id/cardview"/>
        </targets>
    </slide>
    <fade>
        <targets>
            <target android:excludeId="@android:id/statusBarBackground"/>
            <target android:excludeId="@android:id/navigationBarBackground"/>
            <target android:excludeId="@id/cardview"/>
        </targets>
    </fade>

</transitionSet>
sharing_shared_element_enter.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
               android:interpolator="@android:interpolator/accelerate_decelerate">
    <changeBounds/>
    <arcMotion
        android:maximumAngle="90"
        android:minimumHorizontalAngle="90"
        android:minimumVerticalAngle="0"/>
</transitionSet>
sharing_item_chosen.xml
<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeBounds/>
    <fade>
        <targets>
            <target android:excludeId="@id/content_root"/>
        </targets>
    </fade>
    <changeImageTransform android:startDelay="@android:integer/config_mediumAnimTime"/>
</transitionSet>
    Activity transition definitions can be declared into theme style and our res/values/styles.xml contains all this:
styles.xml
<resources>

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AppTheme.Main">
        <item name="android:windowExitTransition">@transition/main_exit</item>
        <item name="android:windowReenterTransition">@transition/main_reenter</item>
    </style>

    <style name="AppTheme.Detail">
        <item name="android:windowTranslucentStatus">true</item>
        <item name="android:windowAllowEnterTransitionOverlap">false</item>
        <item name="android:windowEnterTransition">@transition/detail_enter</item>
    </style>

    <style name="AppTheme.Sharing">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@color/black</item>
        <item name="android:windowTranslucentStatus">true</item>
        <item name="android:windowSharedElementEnterTransition">@transition/sharing_shared_element_enter</item>
    </style>

    <style name="ShareItemView">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:background">?android:attr/selectableItemBackgroundBorderless</item>
        <item name="android:textAppearance">@style/TextAppearance.AppCompat.Medium.Inverse</item>
    </style>

</resources>
    And never forget to use the correct theme for each activity in AndroidManifest.xml:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.devexchanges.uimotion">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme.Main">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".DetailsActivity"
            android:theme="@style/AppTheme.Detail"/>
        <activity android:name=".SharingActivity"/>
    </application>

</manifest>

Transition from Main screen to Detail screen

    When user clicks on some image item, we must start the DetailsActivity with some information that indicates we’re starting a Customized Activity Transition. In MainActivity we have:
    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.rose) {
            openDetailActivity(R.drawable.rose, "Rose", view);
        } else if (view.getId() == R.id.sunflower) {
            openDetailActivity(R.drawable.sunflower, "Sunflower", view);
        } else {
            openDetailActivity(R.drawable.tulip, "Tulip", view);
        }
    }

    private void openDetailActivity(int drawable, String title, View view) {
        ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(this, view, getString(R.string.picture_transition_name));
        Intent intent = new Intent(this, DetailsActivity.class);
        intent.putExtra(DetailsActivity.EXTRA_DRAWABLE, drawable);
        intent.putExtra(DetailsActivity.EXTRA_TITLE, title);

        startActivity(intent, options.toBundle());
    }
    The most important method is ActivityOptions.makeSceneTransitionAnimation(). It create an object containing information about our scene transition animation.
    As you see, I pass drawable id and string title  from MainActivity to setup  CollapsingToolbarLayout and ImageView in DetailsActivity. To finish the our motion from main to details screen we just scale up the share button when the transition is ended:
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_details);
        setSupportActionBar((Toolbar) findViewById(R.id.toolbar));

        int drawable = getIntent().getExtras().getInt(EXTRA_DRAWABLE);
        CharSequence title = getIntent().getExtras().getCharSequence(EXTRA_TITLE);

        CollapsingToolbarLayout collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);
        collapsingToolbarLayout.setTitle(title);

        ImageView pictureView = (ImageView) findViewById(R.id.picture);
        pictureView.setImageResource(drawable);
        pictureView.setContentDescription(title);

        btnShare = findViewById(R.id.btn_share);
        textView = (TextView) findViewById(R.id.text);

        if (drawable == R.drawable.rose) {
            textView.setText(getString(R.string.rose));
        } else if (drawable == R.drawable.tulip) {
            textView.setText(getString(R.string.tulip));
        } else textView.setText(getString(R.string.sunflower));

        if (savedInstanceState == null) {
            btnShare.setScaleX(0);
            btnShare.setScaleY(0);
            getWindow().getEnterTransition().addListener(new TransitionAdapter() {
                @Override
                public void onTransitionEnd(Transition transition) {
                    getWindow().getEnterTransition().removeListener(this);
                    btnShare.animate().scaleX(1).scaleY(1);
                }
            });
        }
    }
    But if we are scaling up the share button when the Activity is opened then we have to scale down when the activity is finished:
    @Override
    public void onBackPressed() {
        btnShare.animate().scaleX(0).scaleY(0).setListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                supportFinishAfterTransition();
            }
        });
    }
    And when running app, we have this output:

Transition from Detail screen to Sharing screen

    Launching SharingActivity after click on the FloatingActionButton:
btnShare.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(DetailsActivity.this,
                        btnShare, getString(R.string.share_transition_name));
                Intent intent = new Intent(DetailsActivity.this, SharingActivity.class);
                startActivity(intent, options.toBundle());
            }
        });
    In SharingActivity, we have to setup initial states before the animation begin:
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        rootView = (ViewGroup) findViewById(R.id.content_root);
        backgroundView = (ImageView) findViewById(R.id.background);
        btnFacebook = findViewById(R.id.facebook);
        btnInstagram = findViewById(R.id.instagram);
        btnTwitter = findViewById(R.id.twitter);
        btnGoogle = findViewById(R.id.google_plus);

        if (savedInstanceState == null) {
            // Setup initial states
            backgroundView.setVisibility(View.INVISIBLE);
            btnGoogle.setAlpha(0);
            btnTwitter.setAlpha(0);
            btnFacebook.setAlpha(0);
            btnInstagram.setAlpha(0);
        }

        getWindow().getSharedElementEnterTransition().addListener(new TransitionAdapter() {
            @Override
            public void onTransitionEnd(Transition transition) {
                getWindow().getSharedElementEnterTransition().removeListener(this);
                revealTheBackground();
                showTheItems();
            }
        });

        ...
    }
    The main work in SharingActivity is handling share items (buttons) click. The pure Transition Framework was added since Android API 19. This framework animates the views at runtime by changing some of their property values over time. One of the features is the ability of running animations based on the changes between starting and ending view property values:
    @Override
    public void onClick(View view) {
        showToast(view.getId());
        // Load the transition
        Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.sharing_item_chosen);
        // Finish this Activity when the transition is ended
        transition.addListener(new TransitionAdapter() {
            @Override
            public void onTransitionEnd(Transition transition) {
                finish();
                // Override default transition to fade out
                overridePendingTransition(0, android.R.anim.fade_out);
            }
        });
        // Capture current values in the scene root and then post a request to run a transition on the next frame
        TransitionManager.beginDelayedTransition(rootView, transition);

        // 1. Item chosen
        RelativeLayout.LayoutParams layoutParams =
                new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
        layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT);
        view.setLayoutParams(layoutParams);

        // 2. Rest of items
        View[] itemViews = {btnFacebook, btnInstagram, btnTwitter, btnGoogle};
        for (View itemView : itemViews) {
            if (itemView != view) {
                itemView.setVisibility(View.INVISIBLE);
            }
        }

        // 3. Background
        double diagonal = Math.sqrt(rootView.getHeight() * rootView.getHeight() + rootView.getWidth() * rootView.getWidth());
        float radius = (float) (diagonal / 2f);
        int h = backgroundView.getDrawable().getIntrinsicHeight();
        float scale = radius / (h / 2f);
        Matrix matrix = new Matrix(backgroundView.getImageMatrix());
        matrix.postScale(scale, scale, backgroundView.getWidth() / 2f, backgroundView.getHeight() / 2f);
        backgroundView.setScaleType(ImageView.ScaleType.MATRIX);
        backgroundView.setImageMatrix(matrix);
    }
    Moreover, override onBackPressed() to start the hide animation of item and background:
private void hideTheBackground() {
        Animator hide = createRevealAnimator(false);
        hide.setStartDelay(defaultAnimDuration);
        hide.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                backgroundView.setVisibility(View.INVISIBLE);
                supportFinishAfterTransition();
            }
        });
        hide.start();
    }

    @Override
    public void onBackPressed() {
        hideTheItems();
        hideTheBackground();
    }

    private void hideTheItems() {
        View[] itemViews = {btnFacebook, btnInstagram, btnTwitter, btnGoogle};
        for (int i = 0; i < itemViews.length; i++) {
            View itemView = itemViews[i];
            long startDelay = (defaultAnimDuration / itemViews.length) * (itemViews.length - i);
            itemView.animate().alpha(0).setStartDelay(startDelay);
        }
    }
    And we'll have this result:

Final thoughts

    I have presented a simple project about applying motion in our application to avoid a bad User Experience. In this sample app, we saw how to build beautiful apps with meaningful and delightful motion. And below are some links where you can go deeper into Android Motion:

Share


Previous post
« Prev Post
Next post
Next Post »