Android Transitions 1: Introduction

In Android KitKat (API level 19) Google introduces a new system for animating layout changes. Before KitKat the only way to animate layout changes in XML was to set the flag animateLayoutChange on a ViewGroup . This did not give you any control on how the transition was animated. Alternatively, you could use LayoutTransition programmatically. This gave you more control but was cumbersome.

Using the new API level 19 Scene and Transition system lets you specify transitions between layouts conveniently in XML resource files and gives you fine grained control on how the transition should be animated. What’s more, you can pre-define a number of different layouts, called scenes, and specify the transitions between them.

The new Scene and Transition system is especially useful when you have a context sensitive layout that can change depending on some parameter. Imagine you have an activity that displays different elements depending on the user’s choice. When the user chooses some option a View is added or removed from the user interface. This is exactly what the new system is aimed at.

In this tutorial I will introduce you to the basics of the Scene and Transition system. In the next tutorial I will present a more realistic example.

Defining Scenes

A scene is defined by a layout XML resource that can be included in a containing layout. To get a feeling for this, let’s first define a layout for our activity.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:id="@+id/container" android:layout_weight="1" android:layout_width="wrap_content" android:layout_height="0dp" android:layout_gravity="center"> </RelativeLayout> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="Change Scenes" android:onClick="changeScenes" /> </LinearLayout>

The main linear layout contains an empty RelativeLayout and a Button. The RelativeLayout will act as a container for the scenes that I will define next. This is reflected in the id that I have given to the layout. Below the container is a button. We will use this button to change the scenes later.

So how do you define a scene? It is actually very easy! All you have to do is to create another layout XML resource with the contents of the scene in it. The scene I will create contains just two TextViews, A and B.

<merge xmlns:android="http://schemas.android.com/apk/res/android" > <TextView android:id="@+id/textA" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A" android:textSize="48sp" /> <TextView android:id="@+id/textB" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/textA" android:text="B" android:textSize="48sp" /> </merge>

I use the ids textA and textB to identify the two text views. The elements are contained within a merge element. Because we will be placing the scenes within the relative layout container of the main activity, the scene does not need a surrounding layout itself. I have used the layout_toRightOf property because I know that the scene will be added to a relative layout. The code above is saved in a file called scene01.xml in the res/layout folder.

Let’s create another scene, but this time with the two text views in reverse order.

<merge xmlns:android="http://schemas.android.com/apk/res/android" > <TextView android:id="@+id/textB" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="B" android:textSize="48sp" /> <TextView android:id="@+id/textA" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/textB" android:text="A" android:textSize="48sp" /> </merge>

This layout is saved in the file scene02.xml in the res/layout folder.

Now that we have created our main layout and two scenes we create the main activity.

public class MainActivity extends Activity { ViewGroup container; Scene current; Scene other; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); container = (ViewGroup) findViewById(R.id.container); current = Scene.getSceneForLayout(container, R.layout.scene01, this); other = Scene.getSceneForLayout(container, R.layout.scene02, this); current.enter(); } }

The activity holds a reference to the container layout defined within the activity’s main layout. This is retrieved in the standard way, using findViewById . Next we create two scenes from the two layouts that we defined. The easiest way of doing this is by using the static factory method, getSceneForLayout , defined inside the Scene class. getSceneForLayout takes three arguments. The first argument is the container layout that the scene should be added into. The second argument is the resource id for the layout for the scene. The third argument is the context. Usually this would be the reference to the activity. We create two scenes from the two layouts and call them current and other .

Finally we need to make sure that the current scene is made visible. The easiest way to do this is by calling the method Scene.enter of the scene that we want to show.

Now that we managed to create our two scene objects and show one the them in our main layout, what about changing scenes? Remember we added a button to the main layout. If you look in the XML file you will see that we intended to call a method called changeScenes . So let’s implement that method inside the MainActivity .

public void changeScenes(View view) { Scene tmp = other; other = current; current = tmp; TransitionManager.go(current); }

All we do in this method is to swap the current and other scenes. Then we call the static method TransitionManager.go and pass it new current scene. This will replace the scene that is currently shown with the new scene. What happens in detail is that the transition manager will look at the contents of the container and compare them with the contents of the new scene. Views that have matching ids in both scenes will be paired and a default transition will be generated to animate the changes from one scene to the next. The default transitions consist of moving views that are in a different location on the screen and fading in or out those views that appear or disappear. A look under the bonnet of the transition system shows that the systems generates property animations to then animate the changes. After the transition has finished, the contents of the container will have been completely replaced by the contents of the new scene.

On the right you can see the resulting animation. You see that both text views swap their position every time the button is pressed.

Customising the Transition

In the example above we have used the default transition to change between the two scenes. This default transition is specified by the AutoTransition class. We can change the transition and customise it simply by using XML resource files. Any resource files that are specific to the Scene and Transition system go in the res/transition folder. Let’s change the duration of the transition to 1 second and also the interpolator to my favourite BounceInterpolator . We create a file called mytransition.xml in the res/transition folder with the following content.

<changeBounds xmlns:android="http://schemas.android.com/apk/res/android" android:duration="2000" android:interpolator="@android:anim/bounce_interpolator" />

The changeBounds tag specifies the type of transition to use. In this case it will use the ChangeBounds class that animates changes in position and size, but not changes in visibility. Because our scenes are simple and none of the content appears or disappears, the ChangeBounds transition is sufficient for now.

Next we need to tell the transition manager to use the transition for scene changes. We create a new member in our MainActivity.

Transition mytransition;

This object will hold the transition. It is initialised in the onCreate method using the TransitionInflater .

mytransition = TransitionInflater.from(this) .inflateTransition(R.transition.mytransition);

The static method TransitionInflater.from creates a transition inflater with the main activity as context. Then we can use inflateTransition with the resource id of our XML file to inflate the transition. Now all we have to do is to pass mytransition to the transition manager. To do this we change the two lines in the main activity that start the transition,

TransitionManager.go(current);

to

TransitionManager.go(current, mytransition);

The result can again be seen in the animation on the right. The transition now takes longer and the effect of the BounceInterpolator can be clearly seen.

Adding and Removing Views

Until now the scene transitions only involved moving views around on the screen. In some cases this is sufficient but quite regularly you will want to add or remove views from a scene. This is also easy but you have to be a little more careful. As an example I will create two scenes, the first scene will have three text views, called A, B and C. In the second scene the middle text view, B, has been removed and the views A and C sit next to each other. To begin with, we will go back to the default transition. So the scenes are simply started using

TransitionManager.go(current);

just as in the first example. The scene01.xml now contains all three text views.

<merge xmlns:android="http://schemas.android.com/apk/res/android" > <TextView android:id="@+id/textA" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A" android:textSize="48sp" /> <TextView android:id="@+id/textB" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/textA" android:text="B" android:textSize="48sp" /> <TextView android:id="@+id/textC" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/textB" android:text="C" android:textSize="48sp" /> </merge>

Naively you would think that scene02.xml should only contain the text views which are still visible.

<merge xmlns:android="http://schemas.android.com/apk/res/android" > <TextView android:id="@+id/textA" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A" android:textSize="48sp" /> <TextView android:id="@+id/textC" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/textA" android:text="C" android:textSize="48sp" /> </merge>

The result can be seen on the right. Note how the view textB fades in nicely but just disappears suddenly when removed. For some reason the fade out transition does not work on views that are simply removed in this way from the layout. The solution is to keep textB in the scene but with the visibility set to invisible .

<merge xmlns:android="http://schemas.android.com/apk/res/android" > <TextView android:id="@+id/textA" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A" android:textSize="48sp" /> <TextView android:id="@+id/textB" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/textA" android:text="B" android:textSize="48sp" android:visibility="invisible" /> <TextView android:id="@+id/textC" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/textA" android:text="C" android:textSize="48sp" /> </merge>

Note how the position of textC specified to be is right of textA . This makes the two outer Text Views move together in the second scene. Now the text fades in and out properly. Note also that the visibility has to be set to invisible and not to gone for this to work. I have also tried to set the width and height of the middle Text View to 0dp in the second scene and even in that case the fade out will not work properly. This behaviour is not documented and might be considered a bug in the current implementation of the Transition System.

Transition Sets

In the previous example you saw two different types of transition. The ChangeBounds transition moves views from their starting location to their final location on the screen. The Fade transition will fade views in and out when their visibility changes. If you want to customise the transition in this case, the specification becomes slightly more complicated. You need a TransitionSet that contains the information about all the different types of transition. We edit the file mytransition.xml in the res/transition directory.

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:transitionOrdering="sequential"> <fade android:duration="300" android:fadingMode="fade_out" /> <changeBounds android:interpolator="@android:anim/linear_interpolator" android:duration="1000" /> <fade android:duration="300" android:fadingMode="fade_in" /> </transitionSet>

The transitionSet XML tag has a property called transitionOrdering . This can be used to specify whether all the transitions in the set should be played together or in sequence. transitionOrdering="sequential" will play them on after another, while transitionOrdering="together" plays them all at the same time. The transition set defined above contains three elements. A Fade transition that fades out any views that might disappear during the scene change. The fadingMode="fade_out" specifies that, at this point in time, views should be faded out but not in. This is followed by a ChangeBounds transition that moves views around the screen to their final position. In this case I have chosen a linear interpolator and a duration of one second for changing bounds. Finally, another Fade transition is responsible for fading in any views that are added in the scene. This is specified using by android:fadingMode="fade_in" .

To apply this transition set we simply change the code in the main activity that shows a scene back to

TransitionManager.go(current, mytransition);

You might ask yourself why the transition set we defined above actually does what we expect. After all, not all the views need to be faded in or out and not all the views need to be moved. When changing from scene 1 to scene 2 only textB disappears and needs to be faded out, only textA and textC change bounds, and there is no view that needs to be faded in.

Relax, everything is handled the way you would expect. The fade animation with the fading mode set to fade_out will select only those views that disappear during the scene change. Similarly, the fade animation with the fading mode set to fade_in will select only those views that appear during the scene change. What is even more, if there is no view that can be animated with a given transition, the transition will not take up any time. This means that, when changing back from scene 2 to scene 1, the first fade transition will not use up any time and the ChangeBounds transition will start immediately when TransitionManager.go is called.

An animation of the resulting behaviour can be seen on the right.

Transition Targets

In some cases you will want to restrict the animation to specific views. Imagine we want to fade out a number of views from one scene to the next. But we don’t want them all to fade out at the same time, instead we want them to fade out one after the other. This can be achieved using transition targets. We start with two scenes. The first scene, specified in scene01.xml , remains unchanged and contains all three views next to each other. In the second scene all the three views are made invisible. We edit scene02.xml like this.

<merge xmlns:android="http://schemas.android.com/apk/res/android" > <TextView android:id="@+id/textA" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A" android:textSize="48sp" android:visibility="invisible" /> <TextView android:id="@+id/textB" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/textA" android:text="B" android:textSize="48sp" android:visibility="invisible" /> <TextView android:id="@+id/textC" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/textB" android:text="C" android:textSize="48sp" android:visibility="invisible" /> </merge>

The file mytransition.xml is changed in the following way.

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" android:transitionOrdering="sequential" > <fade android:duration="300" android:fadingMode="fade_out" > <targets> <target android:targetId="@id/textA" /> </targets> </fade> <fade android:duration="300" android:fadingMode="fade_out" > <targets> <target android:targetId="@id/textB" /> </targets> </fade> <fade android:duration="300" android:fadingMode="fade_out" > <targets> <target android:targetId="@id/textC" /> </targets> </fade> <fade android:duration="300" android:fadingMode="fade_in" > <targets> <target android:targetId="@id/textC" /> </targets> </fade> <fade android:duration="300" android:fadingMode="fade_in" > <targets> <target android:targetId="@id/textB" /> </targets> </fade> <fade android:duration="300" android:fadingMode="fade_in" > <targets> <target android:targetId="@id/textA" /> </targets> </fade> </transitionSet>

As you can see the custom transition now contains 6 fade elements. One fade in for each view and one fade out for each view. Each transition tag can hold a targets tag which, in turn, can hold any number of target tags. Each target is specified by the id of the view that should be animated with this transition. In this way you can animate different elements of the UI in sequence or use different interpolators or durations for the transition. The result of the transition above can be seen in the animation on the right.

I hope you enjoyed this tutorial. Next time I will be presenting a somewhat more realistic use case where transitions might come in handy.

Follow the author