transitions

scene transitions

A Scene holds the state of a view hierarchy, including the various values (layout-related and otherwise) that those views have.

1. You can load scene from a layout resource or create programmatically.

val sceneRoot: ViewGroup = findViewById(R.id.scene_root)

val scene1: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.scene1, this)
val scene2: Scene = Scene.getSceneForLayout(sceneRoot, R.layout.scene2, this)
val scene3: Scene = Scene(sceneRoot, someViewGroup)

2. Prepare a transition animation. There are built-in animations that you can load from a transition resource or create programmatically:

  • AutoTransition - default transition. Fade out, move and resize, and fade in views, in that order. You can use <autoTransition/> tag in the transition resource.
  • Fade - fade transition. You can use <fade android:fadingMode="[fade_in | fade_out | fade_in_out]"/> tag.
  • ChangeBounds - moves and resizes views. You can use <changeBounds/> tag.
// res/transition/fade_transition.xml
// <fade xmlns:android="http://schemas.android.com/apk/res/android" />
var fadeTransition: Transition =
    TransitionInflater.from(this)
                      .inflateTransition(R.transition.fade_transition)
// or                       
var fadeTransition2: Transition = Fade()                     

3. Use transition manager.

// run the animation between scenes by passing your end Scene
TransitionManager.go(endingScene, fadeTransition)

There are some known limitations of the transitions framework:

  1. Animations applied to a SurfaceView may not appear correctly. SurfaceView instances are updated from a non-UI thread, so the updates may be out of sync with the animations of other views.
  2. Some specific transition types may not produce the desired animation effect when applied to a TextureView.
  3. Classes that extend AdapterView, such as ListView, manage their child views in ways that are incompatible with the transitions framework. If you try to animate a view based on AdapterView, the device display may hang.
  4. If you try to resize a TextView with an animation, the text will pop to a new location before the object has completely resized. To avoid this problem, do not animate the resizing of views that contain text.

You can monitor a transition state by the Transition.TransitionListener object.

single view transition

To create a delayed transition within a single view hierarchy, follow these steps:

  1. When the event that triggers the transition occurs, call the TransitionManager.beginDelayedTransition() function providing the parent view of all the views you want to change and the transition to use. The framework stores the current state of the child views and their property values.
  2. Make changes to the child views as required by your use case. The framework records the changes you make to the child views and their properties.
  3. When the system redraws the user interface according to your changes, the framework animates the changes between the original state and the new state.
setContentView(R.layout.activity_main)
val labelText = TextView(this).apply {
    text = "Label"
    id = R.id.text
}
val rootView: ViewGroup = findViewById(R.id.mainLayout)
val mFade: Fade = Fade(Fade.IN)
TransitionManager.beginDelayedTransition(rootView, mFade)
rootView.addView(labelText)

activity transitions

Activity transition APIs are available on API 21+ (Android 5.0).

Shared element is element that presents both in the exit activity and the enter activity. Typically it is ImageView, for example, an icon of the person.

You can setup an activity transition in theme or programmatically.

To make a screen transition animation between two activities that have a shared element:

  1. enable window content transitions
  2. specify a shared elements transition
  3. define your transition, for example as an XML resource
  4. assign a common name to the shared elements in both layouts with the android:transitionName attribute
Setup activity transitions
<style name="BaseAppTheme" parent="android:Theme.Material">
  <!-- enable window content transitions -->
  <item name="android:windowActivityTransitions">true</item>

  <!-- specify enter and exit transitions -->
  <item name="android:windowEnterTransition">@transition/explode</item>
  <item name="android:windowExitTransition">@transition/explode</item>

  <!-- specify shared element transitions -->
  <item name="android:windowSharedElementEnterTransition">
    @transition/change_image_transform</item>
  <item name="android:windowSharedElementExitTransition">
    @transition/change_image_transform</item>
</style>
<!--
res/transition/change_image_transform.xml

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
  <changeImageTransform/>
</transitionSet>
-->
with(window) {
    requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS)

    // set an exit transition
    exitTransition = Explode()
    
    // setEnterTransition()
    // setSharedElementEnterTransition()
    // setSharedElementExitTransition()
}

Start activity with activity options:

startActivity(intent,
              ActivityOptions.makeSceneTransitionAnimation(this).toBundle())
// get the element that receives the click event val imgContainerView = findViewById<View>(R.id.img_container) // get the common element for the transition in this activity val androidRobotView = findViewById<View>(R.id.image_small) // define a click listener imgContainerView.setOnClickListener( { val intent = Intent(this, Activity2::class.java) // create the transition animation - the images in the layouts // of both activities are defined with android:transitionName="robot" val options = ActivityOptions .makeSceneTransitionAnimation(this, androidRobotView, "robot") // start the new activity startActivity(intent, options.toBundle()) })

To reverse the scene transition animation when you finish the second activity, call the Activity.finishAfterTransition() function instead of Activity.finish().

To make a scene transition animation between two activities that have more than one shared element, define the shared elements in both layouts with the android:transitionName attribute (or use the View.setTransitionName() function in both activities), and create an ActivityOptions object as follows:

// Rename the Pair class from the Android framework to avoid a name clash
import android.util.Pair as UtilPair
...
val options = ActivityOptions.makeSceneTransitionAnimation(this,
        UtilPair.create(view1, "agreedName1"),
        UtilPair.create(view2, "agreedName2"))

fragment transitions

Unlike activity transitions, you do not need Window.FEATURE_ACTIVITY_TRANSITIONS or Window.FEATURE_CONTENT_TRANSITIONS features for fragment transitions.

Use following methods of fragment

  • setEnterTransition() to set enter transition animation for second fragment
  • setExitTransition() to set exit transition animation for first fragment
setup fragment transition animation
class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }
}

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }
}

// 

Transitions can be defined as xml resource.

Transitions resource examples
<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"/>

<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right" />

Follow next steps to make transition with shared elements:

  • assign a unique transition name to each shared element view
  • be sure that transition names are same in both fragments
  • add shared element views and transition names to the FragmentTransaction
  • set reordering to true in FragmentTransaction
  • set a shared element transition animation, using
    • setSharedElementEnterTransition() - specifies how the view moves from the first fragment to the second fragment
    • setSharedElementReturnTransition() - specifies how the view moves from the second fragment back to the first fragment
Fragment transition with shared elements
val newFragment = NewFragment().apply{
     sharedElementEnterTransition = TransitionInflater
                                     .from(getContext())
                                     .inflateTransition(android.R.transition.move)
     
     // sharedElementReturnTransition = ...
}

getSupportFragmentManager()
        .beginTransaction()
        .setReorderingAllowed(true)
        .addSharedElement(sharedElement, sharedElement.transitionName)
        .replace(R.id.container, newFragment)
        .addToBackStack(null)
        .commit();

In example above default animation is used

<!-- android.R.transition.move -->
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeBounds/>
    <changeTransform/>
    <changeClipBounds/>
    <changeImageTransform/>
</transitionSet>

For RecyclerView's items you can give each item's shared element a unique transition name by assigning them when the ViewHolder is bound. Then you can pass transition name as a fragment's argument to the second fragment.

Bind shared name to ViewHolder
class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val imgView = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
       imgView.transitionName = id.toString()
       // ...
    }
}

postponed fragment transition

In some cases, you might need to postpone your fragment transition for a short period of time. For example, you might need to wait until all views in the entering fragment have been measured and laid out so that Android can accurately capture their start and end states for the transition.

  1. Fragment transaction must allow reordering of fragment state changes, call setReorderingAllowed() method of FragmentTrasaction
  2. To postpone the enter transition, call postponeEnterTransition() method of Fragment within onViewCreated() method.
  3. Start transition with startPostponedEnterTransition() when you ready, for example when all data is loaded.
Postponed transition
val fragment = FragmentB()
supportFragmentManager.commit {
    setReorderingAllowed(true)
    setCustomAnimation(...)
    addSharedElement(view, view.transitionName)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

//--------------------------------------
class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        
        postponeEnterTransition()
     
        Glide.with(this)
            .load(url)
            // start transition after loading image 
            .listener(object : RequestListener<Drawable> {
                override fun onLoadFailed(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }

                override fun onResourceReady(...): Boolean {
                    startPostponedEnterTransition()
                    return false
                }
            })
            .into(headerImage)
    }
}
// postponed transition of fragment with RecycleView // wait when data list is loaded class FragmentA : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { postponeEnterTransition() // Wait for the data to load viewModel.data.observe(viewLifecycleOwner) { // Set the data on the RecyclerView adapter adapter.setData(it) // A ViewTreeObserver.OnPreDrawListener is set on the parent of the // fragment view. This is to ensure that all of the fragment's views // have been measured and laid out and are therefore ready to be drawn // before beginning the postponed enter transition. (view.parent as? ViewGroup)?.doOnPreDraw { startPostponedEnterTransition() } } } }