Android code programming

Historically, transitions between activities and fragments in Android involved animating the entire view hierarchy. However, with Material Design, it is now easier to animate selected Views during a transition to emphasize continuity, and guide users to content in the target Activity/Fragment.

Many times, there are similar widgets between the start and target activities, and shared element transitions, when used effectively, blurs the boundary between both activities, and the switch between both activities becomes less jarring, and feels natural and unforced. Shared element transitions can be used to guide the user to new content, and its position in the new Activity.

Before we begin, note that shared element transitions discussed here require Android 5.0 (API 21) and above, even using the Support libraries. As such, the code is littered with checks for the build version. In addition, while similar, shared element transitions are different for activities and fragments, and so, we will discuss them separately.

Shared elements in Activities

In the sample project, available on github, we implement two activities, StartActivity and EndActivity.
The StartActivity has an ImageView, a Button and a TextView saved in res/layout/activity_start.xml.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="16dp">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_centerHorizontal="true"
        android:src="@drawable/aa_logo_green"
        android:transitionName="@string/activity_image_trans"/>

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/imageView"
        android:text="Simple TextView"
        android:textSize="20sp"/>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/textView"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:text="Click Me"
        style="@style/Widget.AppCompat.Button.Borderless"
        android:onClick="onClick"/>

</RelativeLayout>

The EndActivity has two ImageViews and an EditText, and is saved in res/layout/activity_end.xml.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="16dp">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="220dp"
        android:layout_height="220dp"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentBottom="true"
        android:src="@drawable/aa_logo_green"
        android:transitionName="@string/activity_image_trans"/>

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:hint="An EditText"
        android:textSize="24sp"/>

    <ImageView
        android:id="@+id/smallerImageView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_toLeftOf="@id/imageView"
        android:layout_alignBottom="@id/imageView"
        android:layout_alignParentStart="true"
        android:layout_alignParentLeft="true"
        android:src="@drawable/aa_logo_blue"/>

</RelativeLayout>

Take note of the transitionName attribute in both the StartActivity and EndActivity. This attribute is used to guide track shared elements between both activites. Shared elements do not need to have the same id, and do not even have to be of the same widget type. As will be shown subsequently, you can have a shared element transition from one type of View to virtually any other.

As long as you have defined the same transitionName for both Views, performing a shared element transition becomes pretty straightforward. In StartActivity,

    public void onClick(View view) {
        Intent intent = new Intent(this, EndActivity.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            ActivityOptionsCompat options = ActivityOptionsCompat.
                    makeSceneTransitionAnimation(this, imageView, getString(R.string.activity_image_trans));
            startActivity(intent, options.toBundle());
        }
        else {
            startActivity(intent);
        }
    }

We called ActivityOptionsCompat.makeSceneTransitionAnimation(), since we extend the AppCompatActivity for our Activity classes. The method expects the start activity, the View to transition to the target activity, and the shared element transitionName. (NOTE: If you are not using the Support Libraries, then you would use ActivityOptions rather than ActivityOptionsCompat).

aa_shared_activity_single

However, it is highly likely that your app contains some dynamically generated content, such that setting the transitionName in xml becomes not feasible. Not to worry, the transitionName can be set in code, in both the start and end activities respectively, and the transition will still get executed.

Think about the process flow. Before the transition begins, the target activity’s layout must be known. Hence, the onCreate() method of the target activity must have been called, and the correct view inflated. Therefore, in the onCreate() method, we can identify the target views and set the transitionName.

The code snippet below uses three shared elements, with two of them set dynamically.

    public void onClick(View view) {
        View imageView = findViewById(R.id.imageView);
        View textView = findViewById(R.id.textView);
        View button = findViewById(R.id.button);
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            textView.setTransitionName(getString(R.string.activity_text_trans));
            button.setTransitionName(getString(R.string.activity_mixed_trans));

            Intent intent = new Intent(this, EndActivity.class);
            Pair<View, String> pair1 = Pair.create(imageView, imageView.getTransitionName());
            Pair<View, String> pair2 = Pair.create(textView, textView.getTransitionName());
            Pair<View, String> pair3 = Pair.create(button, button.getTransitionName());
            ActivityOptionsCompat options = ActivityOptionsCompat.
                    makeSceneTransitionAnimation(this, pair1, pair2, pair3);
            startActivity(intent, options.toBundle());
        }
        else {
            startActivity(intent);
        }
    }

We create Pair objects, containing the desired beginning View, and the transitionName for that View. The makeSceneTransitionAnimation() method we use here expects an activity, followed by a list of Pair objects, containing all desired shared transition Views. (Recall that we are using the support library, so we import the android.support.v4.util.Pair class, rather than android.util.Pair)

In EndActivity, we find the two target views that need transitionNames to be set dynamically. The TextView, from StartActivity, transitions into an ImageView, while the Button transitions into an EditText.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_end);

        View smallImageView = findViewById(R.id.smallerImageView);
        View editText = findViewById(R.id.editText);
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            smallImageView.setTransitionName(getString(R.string.activity_text_trans));
            editText.setTransitionName(getString(R.string.activity_mixed_trans));
        }
    }

aa_shared_activity_all

Be careful with shared element transitions. You do not want too many shared elements, which can become distracting and confusing rather than smooth and natural. You want to direct user focus in a non obtrusive manner.

Shared Elements with Fragments

Shared element transitions with Fragments works in an idealistically similar way to Activities shown above. We implement two Fragments, StartFragment and EndFragment.

StartFragment contains a single ImageView, and a ListView. The ImageView has a transitionName attribute set, to guide a static transition.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="80dp"
        android:src="@drawable/nav_image"
        android:transitionName="@string/fragment_image_trans"/>

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/imageView">

    </ListView>
</RelativeLayout>

The EndFragment contains two ImageViews and a single TextView. One of the ImageViews has its transitionName attribute set.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="24dp"
    android:orientation="vertical">

    <ImageView
        android:id="@+id/listImage"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:src="@drawable/aa_logo_blue"/>

    <TextView
        android:id="@+id/smallerImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:text="Sample"
        android:textSize="24sp"/>

    <ImageView
        android:id="@+id/otherImage"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_marginTop="24dp"
        android:src="@drawable/nav_image"
        android:transitionName="@string/fragment_image_trans"/>
</RelativeLayout>

Shared element transitions between activities has a sane default transition, that works pretty much as expected. For fragments, however, you have to specify a Transition. We define a transition set in res/transition, called change_image_trans. The transition set contains two transition types, changeTransform and changeBounds. changeTransform captures scale and rotation for Views before and after the scene change, while changeBounds captures the layout bounds of target views before and after the scene change. Both transition types also handle the animation of the changes between both target Views. As a result, we can be certain our Views would scale up or down in size as necessary, in addition to starting and ending at the correct location on screen.

<?xml version="1.0" encoding="utf-8"?>
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
    <changeTransform />
    <changeBounds />
</transitionSet>

To animate the transition in a FragmentTransaction, we call setSharedElementEnterTransition() and setEnterTransition() on the target fragment (EndFragment). We also call setSharedElementReturnTransaction() and setExitTransition() on the start fragment (StartFragment).
Finally, while building the FragmentTransaction, we call addSharedElement(), containing the initial View, and the transitionName.

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        ImageView staticImage = (ImageView) getView().findViewById(R.id.imageView);

        EndFragment endFragment = new EndFragment();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            setSharedElementReturnTransition(TransitionInflater.from(
                    getActivity()).inflateTransition(R.transition.change_image_trans));
            setExitTransition(TransitionInflater.from(
                    getActivity()).inflateTransition(android.R.transition.fade));

            endFragment.setSharedElementEnterTransition(TransitionInflater.from(
                    getActivity()).inflateTransition(R.transition.change_image_trans));
            endFragment.setEnterTransition(TransitionInflater.from(
                    getActivity()).inflateTransition(android.R.transition.fade));
        }

        Bundle bundle = new Bundle();
        bundle.putString("ACTION", textView.getText().toString());
        bundle.putParcelable("IMAGE", ((BitmapDrawable) imageView.getDrawable()).getBitmap());
        endFragment.setArguments(bundle);
        FragmentManager fragmentManager = getFragmentManager();
        fragmentManager.beginTransaction()
                .replace(R.id.container, endFragment)
                .addToBackStack("Payment")
                .addSharedElement(staticImage, getString(R.string.fragment_image_trans))
                .commit();
    }

aa_shared_fragment_single

To implement multiple shared elements as well as dynamically generated shared elements, we use a ListView, with list items containing an ImageView and a TextView. In the ListAdapter, we set a transitionName for both the ImageView and the TextView for every list item.

class MyListAdapter extends ArrayAdapter<String> {

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        View view = convertView;

        if (view == null) {
            view = LayoutInflater.from(getContext()).inflate(R.layout.list_item, null);
        }

        TextView textView = (TextView) view.findViewById(R.id.textView);

        ImageView imageView = (ImageView) view.findViewById(R.id.imageView);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            textView.setTransitionName("transtext" + position);
            imageView.setTransitionName("transition" + position);
        }

        return view;
    }
}

Setting up the transition is similar to the single element transition. The main difference here is we fetch the two dynamically generated transition names, and wrap them in a bundle for the target fragment. Finally, for each shared element, we call the addSharedElement() method.

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        String imageTransitionName = "";
        String textTransitionName = "";

        ImageView imageView = (ImageView) view.findViewById(R.id.imageView);
        TextView textView = (TextView) view.findViewById(R.id.smallerImageView);

        ...
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            ...
            imageTransitionName = imageView.getTransitionName();
            textTransitionName = textView.getTransitionName();
        }

        bundle.putString("TRANS_NAME", imageTransitionName);
        bundle.putString("TRANS_TEXT", textTransitionName);
        endFragment.setArguments(bundle);
        ...
        fragmentManager.beginTransaction()
                .replace(R.id.container, endFragment)
                .addToBackStack("Payment")
                .addSharedElement(imageView, imageTransitionName)
                .addSharedElement(textView, textTransitionName)
                .addSharedElement(staticImage, getString(R.string.fragment_image_trans))
                .commit();
    }

In EndFragment, we retrieve the transition names, and update the target Views.

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        Bundle bundle = getArguments();
        String actionTitle = "";
        Bitmap imageBitmap = null;
        String transText = "";
        String transitionName = "";

        if (bundle != null) {
            transitionName = bundle.getString("TRANS_NAME");
            actionTitle = bundle.getString("ACTION");
            imageBitmap = bundle.getParcelable("IMAGE");
            transText = bundle.getString("TRANS_TEXT");
        }

        getActivity().setTitle(actionTitle);
        View view = inflater.inflate(R.layout.fragment_end, container, false);

        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            view.findViewById(R.id.listImage).setTransitionName(transitionName);
            view.findViewById(R.id.smallerImageView).setTransitionName(transText);
        }

        ((ImageView) view.findViewById(R.id.listImage)).setImageBitmap(imageBitmap);
        ((TextView) view.findViewById(R.id.smallerImageView)).setText(actionTitle);

        return view;
    }

aa_shared_fragment_all

Wrap-up

Well planned transitions and animations provides an app with a premium feel, and will be pleasurable for users. Movements between activities and fragments will appear to flow naturally, and will also guide a user’s focus towards the relationship between the new screen and the previous screen. The complete source code for the tutorial is available on GitHub, and can be used and modified to your heart’s content. Happy coding.

Comments
Read comments