1. Code
  2. Mobile Development
  3. Android Development

Introduction to the New Lollipop Activity Transitions

Scroll to top
What You'll Be Creating

Introduction

One of the most interesting aspects of the Material Design specifications is the visual continuity between activities. With just a few lines of code, the new Lollipop APIs allow you to meaningfully transition between two activities, thanks to seamless and continuous animations. This breaks the classic activity boundaries of the previous Android versions and allows the user to understand how elements go from one point to another.

In this tutorial, I will show you how to achieve this result, making a sample application consistent with Google’s Material Design guidelines.

Prerequisites

In this tutorial, I'll assume that you are already familiar with Android development and that you use Android Studio as your IDE. I'll use Android intents extensively, assuming a basic knowledge of the activity lifecycle, and the new RecyclerView widget introduced with API 21, last June. I'm not going to dive into the details of this class, but, if you're interested, you can find a great explanation in this Tuts+ tutorial.

1. Create the First Activity

The basic structure of the application is straightforward. There are two activities, a main one, MainActivity.java, whose task it is to display a list of items, and a second one, DetailActivity.java, which will show the details of the item selected in the previous list.

Step 1: The RecyclerView Widget

To show the list of items, the main activity will use the RecyclerView widget introduced in Android Lollipop. The first thing you need to do is, add the following line to the dependencies section in your project’s build.grade file to enable backward compatibility:

1
compile 'com.android.support:recyclerview-v7:+'

Step 2: Data Definition

For the sake of brevity, we will not define an actual database or a similar source of data for the application. Instead, we will use a custom class, Contact. Each item will have a name, a color, and basic contact information associated to it. This is what the implementation of the Contact class looks like:

1
public class Contact {
2
3
    // The fields associated to the person

4
    private final String mName, mPhone, mEmail, mCity, mColor;
5
6
    Contact(String name, String color, String phone, String email, String city) {
7
        mName = name; mColor = color; mPhone = phone; mEmail = email; mCity = city;
8
    }
9
10
    // This method allows to get the item associated to a particular id,

11
    // uniquely generated by the method getId defined below

12
    public static Contact getItem(int id) {
13
        for (Contact item : CONTACTS) {
14
            if (item.getId() == id) {
15
                return item;
16
            }
17
        }
18
        return null;
19
    }
20
21
    // since mName and mPhone combined are surely unique,

22
    // we don't need to add another id field

23
    public int getId() {
24
        return mName.hashCode() + mPhone.hashCode();
25
    }
26
27
    public static enum Field {
28
        NAME, COLOR, PHONE, EMAIL, CITY
29
    }
30
    public String get(Field f) {
31
        switch (f) {
32
            case COLOR: return mColor;
33
            case PHONE: return mPhone;
34
            case EMAIL: return mEmail;
35
            case CITY: return mCity;
36
            case NAME: default: return mName;
37
        }
38
    }
39
40
}

You will end up with a nice container for the information you care about. But we need to fill it with some data. At the top of the Contact class, add the following piece of code to populate the data set.

By defining the data as public and static, every class in the project is able to read it. In a sense, we mimic the behavior of a database with the exception that we are hardcoding it into a class.

1
public static final Contact[] CONTACTS = new Contact[] {
2
    new Contact("John", "#33b5e5", "+01 123456789", "john@example.com", "Venice"),
3
    new Contact("Valter", "#ffbb33", "+01 987654321", "valter@example.com", "Bologna"),
4
    new Contact("Eadwine", "#ff4444", "+01 123456789", "eadwin@example.com", "Verona"),
5
    new Contact("Teddy", "#99cc00", "+01 987654321", "teddy@example.com", "Rome"),
6
    new Contact("Ives", "#33b5e5", "+01 11235813", "ives@example.com", "Milan"),
7
    new Contact("Alajos", "#ffbb33", "+01 123456789", "alajos@example.com", "Bologna"),
8
    new Contact("Gianluca", "#ff4444", "+01 11235813", "me@gian.lu", "Padova"),
9
    new Contact("Fane", "#99cc00", "+01 987654321", "fane@example.com", "Venice"),
10
};

Step 3: Defining the Main Layouts

The layout of the main activity is simple, because the list will fill the entire screen. The layout includes a RelativeLayout as the root—but it can just as well be a LinearLayout too—and a RecyclerView as its only child.

1
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
2
    android:layout_width="match_parent"
3
    android:layout_height="match_parent"
4
    android:background="#f5f5f5">
5
6
    <android.support.v7.widget.RecyclerView
7
        android:layout_width="match_parent"
8
        android:layout_height="match_parent"
9
        android:id="@+id/rv" />
10
11
</RelativeLayout>

Because the RecyclerView widget arranges subelements and nothing more, you also need to design the layout of a single list item. We want to have a colored circle to the left of each item of the contact list so you first have to define the drawable circle.xml.

1
<shape
2
    xmlns:android="http://schemas.android.com/apk/res/android"
3
    android:shape="oval">
4
    <solid
5
        android:color="#000"/>
6
    <size
7
        android:width="32dp"
8
        android:height="32dp"/>
9
</shape>

You now have all the elements needed to define the layout of the list item.

1
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
    android:layout_width="match_parent"
3
    android:layout_height="82dp"
4
    android:padding="@dimen/activity_horizontal_margin"
5
    android:background="?android:selectableItemBackground"
6
    android:clickable="true"
7
    android:focusable="true"
8
    android:orientation="vertical" >
9
10
    <View
11
        android:id="@+id/CONTACT_circle"
12
        android:layout_width="40dp"
13
        android:layout_height="40dp"
14
        android:background="@drawable/circle"
15
        android:layout_centerVertical="true"
16
        android:layout_alignParentLeft="true"/>
17
18
    <LinearLayout
19
        android:layout_width="wrap_content"
20
        android:layout_height="wrap_content"
21
        android:layout_centerVertical="true"
22
        android:layout_toRightOf="@+id/CONTACT_circle"
23
        android:layout_marginLeft="@dimen/activity_horizontal_margin"
24
        android:orientation="vertical">
25
26
        <TextView
27
            android:id="@+id/CONTACT_name"
28
            android:layout_width="wrap_content"
29
            android:layout_height="wrap_content"
30
            android:text="Jonh Doe"
31
            android:textColor="#000"
32
            android:textSize="18sp"/>
33
34
        <TextView
35
            android:id="@+id/CONTACT_phone"
36
            android:layout_width="wrap_content"
37
            android:layout_height="wrap_content"
38
            android:text="+01 123456789"
39
            android:textColor="#9f9f9f"
40
            android:textSize="15sp"/>
41
42
    </LinearLayout>
43
44
</RelativeLayout>

Step 4: Show the Data Using the RecyclerView

We have almost arrived at the end of the first part of the tutorial. You still have to write the RecyclerView.ViewHolder and the RecyclerView.Adapter, and assign everything to the associated view in the onCreate method of the main activity. In this case, the RecyclerView.ViewHolder must also be able to handle clicks so you will need to add a specific class capable of doing so. Let's start defining the class responsible for click handling.

1
public class RecyclerClickListener implements RecyclerView.OnItemTouchListener {
2
3
    private OnItemClickListener mListener;
4
    GestureDetector mGestureDetector;
5
6
    public interface OnItemClickListener {
7
        public void onItemClick(View view, int position);
8
    }
9
10
    public RecyclerClickListener(Context context, OnItemClickListener listener) {
11
        mListener = listener;
12
        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
13
            @Override public boolean onSingleTapUp(MotionEvent e) {
14
                return true;
15
            }
16
        });
17
    }
18
19
    @Override public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
20
        View childView = view.findChildViewUnder(e.getX(), e.getY());
21
        if (childView != null && mListener != null && mGestureDetector.onTouchEvent(e)) {
22
            mListener.onItemClick(childView, view.getChildPosition(childView));
23
            return true;
24
        }
25
        return false;
26
    }
27
28
    @Override public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) { }
29
30
}

It is necessary to specify the RecyclerView.Adapter, which I will call it DataManager. It is responsible for loading the data and inserting it into the views of the list. This data manager class will also contain the definition of the RecyclerView.ViewHolder.

1
public class DataManager extends RecyclerView.Adapter<DataManager.RecyclerViewHolder> {
2
3
    public static class RecyclerViewHolder extends RecyclerView.ViewHolder {
4
5
        TextView mName, mPhone;
6
        View mCircle;
7
8
        RecyclerViewHolder(View itemView) {
9
            super(itemView);
10
            mName = (TextView) itemView.findViewById(R.id.CONTACT_name);
11
            mPhone = (TextView) itemView.findViewById(R.id.CONTACT_phone);
12
            mCircle = itemView.findViewById(R.id.CONTACT_circle);
13
        }
14
    }
15
16
    @Override
17
    public RecyclerViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
18
        View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.contact_item, viewGroup, false);
19
        return new RecyclerViewHolder(v);
20
    }
21
22
    @Override
23
    public void onBindViewHolder(RecyclerViewHolder viewHolder, int i) {
24
        // get the single element from the main array

25
        final Contact contact = Contact.CONTACTS[i];
26
        // Set the values

27
        viewHolder.mName.setText(contact.get(Contact.Field.NAME));
28
        viewHolder.mPhone.setText(contact.get(Contact.Field.PHONE));
29
        // Set the color of the shape

30
        GradientDrawable bgShape = (GradientDrawable) viewHolder.mCircle.getBackground();
31
        bgShape.setColor(Color.parseColor(contact.get(Contact.Field.COLOR)));
32
    }
33
34
    @Override
35
    public int getItemCount() {
36
        return Contact.CONTACTS.length;
37
    }
38
}

Finally, add the following code to the onCreate method, below setContentView. The main activity is ready.

1
RecyclerView rv = (RecyclerView) findViewById(R.id.rv); // layout reference

2
3
LinearLayoutManager llm = new LinearLayoutManager(this);
4
rv.setLayoutManager(llm);
5
rv.setHasFixedSize(true); // to improve performance

6
7
rv.setAdapter(new DataManager()); // the data manager is assigner to the RV

8
rv.addOnItemTouchListener( // and the click is handled

9
    new RecyclerClickListener(this, new RecyclerClickListener.OnItemClickListener() {
10
        @Override public void onItemClick(View view, int position) {
11
            // STUB:

12
            // The click on the item must be handled

13
        }
14
    }));

This is what the application looks like if you build and run it.

First Activity when completedFirst Activity when completedFirst Activity when completed

2. Create the Details Activity

Step 1: The Layout

The second activity is much simpler. It takes the ID of the contact selected and retrieves the additional information that the first activity doesn't show.

From a design point of view, the layout of this activity is critical since it's the most important part of the application. But for what concerns the XML, it's trivial. The layout is a series of TextView instances positioned in a pleasant way, using RelativeLayout and LinearLayout. This is what the layout looks like:

1
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
3
    android:layout_height="match_parent"
4
    android:orientation="vertical">
5
    
6
    <ImageView
7
        android:layout_width="match_parent"
8
        android:layout_height="200dp"
9
        android:scaleType="centerCrop"
10
        android:src="@mipmap/material_wallpaper"/>
11
12
    <RelativeLayout
13
        android:layout_width="match_parent"
14
        android:layout_height="82dp"
15
        android:padding="@dimen/activity_vertical_margin">
16
17
        <View
18
            android:id="@+id/DETAILS_circle"
19
            android:layout_width="48dp"
20
            android:layout_height="48dp"
21
            android:background="@drawable/circle"
22
            android:layout_centerVertical="true"
23
            android:layout_alignParentLeft="true"/>
24
25
        <TextView
26
            android:id="@+id/DETAILS_name"
27
            android:layout_width="wrap_content"
28
            android:layout_height="wrap_content"
29
            android:text="Jonh Doe"
30
            android:layout_toRightOf="@+id/DETAILS_circle"
31
            android:layout_marginLeft="@dimen/activity_horizontal_margin"
32
            android:layout_centerVertical="true"
33
            android:textColor="#000"
34
            android:textSize="25sp"/>
35
36
    </RelativeLayout>
37
38
    <LinearLayout
39
        android:layout_width="wrap_content"
40
        android:layout_height="wrap_content"
41
        android:layout_centerVertical="true"
42
        android:padding="@dimen/activity_horizontal_margin"
43
        android:orientation="vertical">
44
        
45
        <RelativeLayout
46
            android:layout_width="match_parent"
47
            android:layout_height="wrap_content">
48
49
            <TextView
50
                android:id="@+id/DETAILS_phone_label"
51
                android:layout_width="wrap_content"
52
                android:layout_height="wrap_content"
53
                android:text="Phone:"
54
                android:textColor="#000"
55
                android:textSize="20sp"/>
56
            
57
            <TextView
58
                android:id="@+id/DETAILS_phone"
59
                android:layout_width="wrap_content"
60
                android:layout_height="wrap_content"
61
                android:layout_toRightOf="@+id/DETAILS_phone_label"
62
                android:layout_marginLeft="@dimen/activity_horizontal_margin"
63
                android:text="+01 123456789"
64
                android:textColor="#9f9f9f"
65
                android:textSize="20sp"/>
66
            
67
        </RelativeLayout>
68
69
        <RelativeLayout
70
            android:layout_width="match_parent"
71
            android:layout_height="wrap_content"
72
            android:layout_marginTop="@dimen/activity_vertical_margin">
73
74
            <TextView
75
                android:id="@+id/DETAILS_email_label"
76
                android:layout_width="wrap_content"
77
                android:layout_height="wrap_content"
78
                android:text="Email:"
79
                android:textColor="#000"
80
                android:textSize="20sp"/>
81
82
            <TextView
83
                android:id="@+id/DETAILS_email"
84
                android:layout_width="wrap_content"
85
                android:layout_height="wrap_content"
86
                android:layout_toRightOf="@+id/DETAILS_email_label"
87
                android:layout_marginLeft="@dimen/activity_horizontal_margin"
88
                android:text="jonh.doe@example.com"
89
                android:textColor="#9f9f9f"
90
                android:textSize="20sp"/>
91
92
        </RelativeLayout>
93
94
        <RelativeLayout
95
            android:layout_width="match_parent"
96
            android:layout_height="wrap_content"
97
            android:layout_marginTop="@dimen/activity_vertical_margin">
98
99
            <TextView
100
                android:id="@+id/DETAILS_city_label"
101
                android:layout_width="wrap_content"
102
                android:layout_height="wrap_content"
103
                android:text="City:"
104
                android:textColor="#000"
105
                android:textSize="20sp"/>
106
107
            <TextView
108
                android:id="@+id/DETAILS_city"
109
                android:layout_width="wrap_content"
110
                android:layout_height="wrap_content"
111
                android:layout_toRightOf="@+id/DETAILS_city_label"
112
                android:layout_marginLeft="@dimen/activity_horizontal_margin"
113
                android:text="Rome"
114
                android:textColor="#9f9f9f"
115
                android:textSize="20sp"/>
116
117
        </RelativeLayout>
118
    </LinearLayout>
119
</LinearLayout>

Step 2: Send and Receive the ID via Intent Extras

Since the two activities are linked by an intent, you need to send some piece of information that allows the second activity to understand of which contact you requested the details.

One option may be using the position variable as a reference. The position of the element in the list corresponds to the position of the element in the array so there should be nothing bad in using this integer as a unique reference.

This would work, but if you take this approach and, for whatever reason, the data set is modified at runtime, the reference won't match the contact you're interested in. This is the reason why it is better to use an ID ad hoc. This information is the getId method defined in the Contact class.

Edit the onItemClick handler of the list of items as shown below.

1
@Override public void onItemClick(View view, int position) {
2
    Intent intent = new Intent(MainActivity.this, DetailsActivity.class);
3
    intent.putExtra(DetailsActivity.ID, Contact.CONTACTS[position].getId());
4
    startActivity(intent);
5
}

The DetailsActivity will receive the information from the Intent extras and construct the correct object using the ID as a reference. This is shown in the following code block.

1
// Before the onCreate

2
public final static String ID = "ID";
3
public Contact mContact;
1
// In the onCreate, after the setContentView method

2
mContact = Contact.getItem(getIntent().getIntExtra(ID, 0));

Just as before in the onCreateViewHolder method of the RecylerView, the views are initialized using the findViewById method and populated using setText. For example, to configure the name field we do the following:

1
mName = (TextView) findViewById(R.id.DETAILS_name);
2
mName.setText(mContact.get(Contact.Field.NAME));

The process is the same for the other fields. The second activity is finally ready.

Second Activity when completedSecond Activity when completedSecond Activity when completed

3. Meaningful Transitions

We have finally arrived at the core of the tutorial, animating the two activities using the new Lollipop method for transitioning using a shared element.

Step 1: Configure Your Project

The first thing you will need to do is edit your theme in the style.xml file in the values-v21 folder. In this way, you enable content transitions and set the entrance and the exit of the views that are not shared between the two activities.

1
<style name="AppTheme" parent="AppTheme.Base"></style>
2
3
<style name="AppTheme.Base" parent="android:Theme.Material.Light">
4
5
    <item name="android:windowContentTransitions">true</item>
6
7
    <item name="android:windowEnterTransition">@android:transition/slide_bottom</item>
8
    <item name="android:windowExitTransition">@android:transition/slide_bottom</item>
9
10
    <item name="android:windowAllowEnterTransitionOverlap">true</item>
11
    <item name="android:windowAllowReturnTransitionOverlap">true</item>
12
    <item name="android:windowSharedElementEnterTransition">@android:transition/move</item>
13
    <item name="android:windowSharedElementExitTransition">@android:transition/move</item>
14
15
</style>

Please note that your project must be targeted to (and thus be compiled with) at least Android API 21.

The animations will be ignored on systems that don't have Lollipop installed. Unfortunately, because of performance reasons, the AppCompat library does not provide complete backward compatibility for these animations.

Step 2: Assign the Transition Name in the Layout Files

Once you've edited your style.xml file, you have to point out the relationship between the two common elements of the views.

In our example, the shared views are the field containing the name of the contact, the one of the phone number, and the colored circle. For each of them, you have to specify a common transition name. For this reason, start adding in the strings.xml resource file the following items:

1
<string name="transition_name_name">transition:NAME</string>
2
<string name="transition_name_circle">transition:CIRCLE</string>
3
<string name=“transition_name_phone”>transition:PHONE</string>

Then, for each of the three pairs, in the layout files add the android:transitionName attribute with the corresponding value. For the colored circle, the code looks like this:

1
<!— In the single item layout: the item we are transitioning *from* —>
2
<View
3
    android:id=“@+id/CONTACT_circle”
4
    android:transitionName=“@string/transition_name_circle”
5
    android:layout_width=“40dp”
6
    android:layout_height=“40dp”
7
    android:background=“@drawable/circle”
8
    android:layout_centerVertical=“true”
9
    android:layout_alignParentLeft=“true”/>
1
<!— In the details activity: the item we are transitioning *to* —>
2
<View
3
    android:id=“@+id/DETAILS_circle”
4
    android:transitionName=“@string/transition_name_circle”
5
    android:layout_width=“48dp”
6
    android:layout_height=“48dp”
7
    android:background=“@drawable/circle”
8
    android:layout_centerVertical=“true”
9
    android:layout_alignParentLeft=“true”/>

Thanks to this attribute, Android will know which views are shared between the two activities and will correctly animate the transition. Repeat the same process for the other two views.

Step 3: Configure the Intent

From a coding point of view, you will need to attach a specific ActivityOptions bundle to the intent. The method you need is makeSceneTransitionAnimation, which takes as parameters the context of the application and as many shared elements as we need. In the onItemClick method of the RecyclerView, edit the previously defined Intent like this:

1
@Override public void onItemClick(View view, int position) {
2
    Intent intent = new Intent(MainActivity.this, DetailsActivity.class);
3
    intent.putExtra(DetailsActivity.ID, Contact.CONTACTS[position].getId());
4
5
    ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
6
            // the context of the activity

7
            MainActivity.this,
8
9
            // For each shared element, add to this method a new Pair item,

10
            // which contains the reference of the view we are transitioning *from*,

11
            // and the value of the transitionName attribute

12
            new Pair<View, String>(view.findViewById(R.id.CONTACT_circle),
13
                    getString(R.string.transition_name_circle)),
14
            new Pair<View, String>(view.findViewById(R.id.CONTACT_name),
15
                    getString(R.string.transition_name_name)),
16
            new Pair<View, String>(view.findViewById(R.id.CONTACT_phone),
17
                    getString(R.string.transition_name_phone))
18
    );
19
    ActivityCompat.startActivity(MainActivity.this, intent, options.toBundle());
20
}

For each shared element to be animated, you will have to add to the makeSceneTransitionAnimation method a new Pair item. Each Pair has two values, the first is a reference to the view you are transitioning from, the second is the value of the transitionName attribute.

Be careful when importing the Pair class. You will need to include the android.support.v4.util package, not the android.util package. Also, remember to use ActivityCompat.startActivity method instead of the startActivity method, because otherwise you will not be able to run your application on environments with API below 16.

That's it. You’re done. It's as simple as that.

Conclusion

In this tutorial you learned how to beautifully and seamlessly transition between two activities that share one or more common elements, allowing for a visually pleasant and meaningful continuity.

You started by making the first of the two activities, whose role it is to display the list of contacts. You then completed the second activity, designing its layout, and implementing a way to pass a unique reference between the two activities. Finally, you looked at the way in which makeSceneTransitionAnimation works, thanks to the XML transitionName attribute.

Bonus Tip: Stylistic Details

To create a true Material Design looking application, as shown in the previous screenshots, you will also need to change the colors of your theme. Edit your base theme in the values-v21 folder to achieve a nice result.

1
<style name=“AppTheme” parent=“AppTheme.Base”>
2
3
    <item name=“android:windowTitleSize”>0dp</item>
4
    <item name=“android:colorPrimary”>@color/colorPrimary</item>
5
    <item name=“android:colorPrimaryDark”>@color/colorPrimaryDark</item>
6
    <item name=“android:colorAccent”>@color/colorAccent</item>
7
    <item name=“android:textColorPrimary”>#fff</item>
8
    <item name=“android:textColor”>#727272</item>
9
    <item name=“android:navigationBarColor”>#303F9F</item>
10
11
</style>
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.