An Introduction to Material Design with Kotlin

In this tutorial you’ll learn how to integrate Material Design into an existing app and create delightful interactions using the animation APIs. By Aaqib Hussain.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

Activity Transitions With Shared Elements

We’ve all seen and wondered about those nice image and text transitions in Google’s app which has been updated to use Material Design. Well, wait no more—you’re about to learn the intricacies of achieving a smooth animation.

Note: Activity transitions, together with shared elements, allow your app to transition between two activities that share common views. For example, you can transition a thumbnail on a list into a larger image on a detail activity, providing continuity of the content.

Between the places list view, MainActivity, and the places detail view, DetailActivity, you’re going to transition the following elements:

  • The image of the place;
  • The title of the place;
  • The background area of the title.

Open row_places.xml and add the following to the declaration of the ImageView tag with an id of placeImage:

android:transitionName="tImage"

And then add this to the LinearLayout tag with an id of placeNameHolder:

android:transitionName="tNameHolder"

Notice that placeName doesn’t have a transition name. This is because it is the child of placeNameHolder, which will transition all of its child views.

In activity_detail.xml, add a transitionName to the ImageView tag with the id placeImage:

android:transitionName="tImage"

And, in a similar fashion, add a transitionName to the LinearLayout tag that has an id of placeNameHolder:

android:transitionName="tNameHolder"

Shared elements between activities that you want to transition should have the same android:transitionName, which is what you’re setting up here. Also, notice that the size of the image, as well as the height of the placeNameHolder, are much larger in this activity. You’re going to animate all of these layout changes during the activity transition to provide some nice visual continuity.

In onItemClickListener() found in MainActivity, update the method to the following:

override fun onItemClick(view: View, position: Int) {
  val intent = DetailActivity.newIntent(this@MainActivity, position)

  // 1
  val placeImage = view.findViewById<ImageView>(R.id.placeImage)
  val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)

  // 2
  val imagePair = Pair.create(placeImage as View, "tImage")
  val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")

  // 3
  val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,
    imagePair, holderPair)
  ActivityCompat.startActivity(this@MainActivity, intent, options.toBundle())
}

After adding this code, you will need to manually add the following import statement to the top of the file as Android Studio cannot automatically determine that this is the intended package.

import android.support.v4.util.Pair

There are a couple of things to highlight here:

  1. You get an instance of both placeImage and placeNameHolder for the given position of the RecyclerView. You’re not relying on Kotlin Android Extensions here since you need the placeImage and placeNameHolder from the specific view.
  2. You create a Pair containing the view and the transitionName for both the image and the text holder view. Note that you will once again have to manually add the import statement to the top of the file: android.support.v4.util.Pair.
  3. To make the activity scene transition with shared views, you pass in your Pair instances and start the activity with your options bundle.

Build and run to see the image transition from the main activity to the detail activity:

However, the animation is a bit jumpy in two areas:

  • The FAB button suddenly appears in DetailActivity.
  • If you tap on a row under the action or navigation bar, that row appears to jump a bit before it transitions.

You’ll solve the FAB button issue first. Open DetailActivity.kt and add the following to windowTransition():

window.enterTransition.addListener(object : Transition.TransitionListener {
  override fun onTransitionEnd(transition: Transition) {
    addButton.animate().alpha(1.0f)
    window.enterTransition.removeListener(this)
  }

  override fun onTransitionResume(transition: Transition) { }
  override fun onTransitionPause(transition: Transition) { }
  override fun onTransitionCancel(transition: Transition) { }
  override fun onTransitionStart(transition: Transition) { }
})

The listener you add to the enter transition is triggered when the window transition ends, which you use to fade in the FAB button. For this to be effective, set the alpha to 0 for the FAB in activity_detail.xml:

android:alpha="0.0"

Build and run! You’ll notice the FAB transition is much smoother!:

As for the action bar and navigation bar issues, begin by updating styles.xml, to set the parent theme to Theme.AppCompat.Light.NoActionBar:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">

Since there is no action bar defined in styles.xml, you’ll have to add it using individual XML views.

Open activity_main.xml and add the following inside LinearLayout, just above the RecyclerView tag:

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

This simply includes a toolbar layout that's provided as part of the starter project into the current layout. Now you need to make a similar change to the detail activity's layout.

Open activity_detail.xml and add the following at the very bottom of the first FrameLayout, just below the closing tag of the inner LinearLayout:

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

Next in MainActivity, you need to initialize the toolbar. Add the following to the bottom of the onCreate() method:

setUpActionBar()

Here you assign the result of the findViewById call to the new field, and then call setUpActionBar(). At the moment it's just an empty method stub. Fix that now by adding the following to setUpActionBar():

    setSupportActionBar(toolbar)
    supportActionBar?.setDisplayHomeAsUpEnabled(false)
    supportActionBar?.setDisplayShowTitleEnabled(true)
    supportActionBar?.elevation = 7.0f

Here you set the action bar to be an instance of your custom toolbar, set the visibility of the title, disable the home button, and add a subtle drop shadow by setting the elevation.

Build and run. You'll notice that nothing much has changed, but these changes have laid the foundations of properly being able to transition the toolbar.

Open MainActivity and replace the existing onItemClickListener with this one:

private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {
  override fun onItemClick(view: View, position: Int) {
    // 1
    val transitionIntent = DetailActivity.newIntent(this@MainActivity, position)
    val placeImage = view.findViewById<ImageView>(R.id.placeImage)
    val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)

    // 2
    val navigationBar = findViewById<View>(android.R.id.navigationBarBackground)
    val statusBar = findViewById<View>(android.R.id.statusBarBackground)

    val imagePair = Pair.create(placeImage as View, "tImage")
    val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")

    // 3
    val navPair = Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)
    val statusPair = Pair.create(statusBar, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)
    val toolbarPair = Pair.create(toolbar as View, "tActionBar")

    // 4
    val pairs = mutableListOf(imagePair, holderPair, statusPair, toolbarPair)
    if (navigationBar != null && navPair != null) {
      pairs += navPair
    }

    // 5
    val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,
      *pairs.toTypedArray())
    ActivityCompat.startActivity(this@MainActivity, transitionIntent, options.toBundle())
  }
}

The differences between the original implementation and this one are thus:

  1. You've renamed the intent to provide more context;
  2. You get references to both the navigation bar and status bar;
  3. You've created three new instances of Pair - one for the navigation bar, one for the status bar, and one for the toolbar;
  4. You've protected against an IllegalArgumentException that occurs on certain devices, such as the Galaxy Tab S2, on which navPair is null.
  5. And finally you've updated the options that are passed to the new activity to include the references to the new views. You've used the Kotlin spread operator * on pairs, after changing it to a typed array.

Great! Build and run, and you’ll see a much smoother animation:

Now if you tap on a row under the action/toolbar or navigation bar, it doesn't jump before the transition; it transitions with the rest of the shared elements, which is much more pleasing to the eye. Switch to the grid view and you'll notice that the transitions work very nicely with that layout as well.

Ta-da! Here is a video of the final app in action: