Activity Animations: shared element transitions demystified

Every now an then you see an app that has some fancy activity animation when transitioning from one Activity to another. We’ll dig deeper into how these transitions are executed and what is happening in the background.

To make our lives easier, Android Engineers introduced a new API from Android 5.0 (API level 21) called „Shared Element Transition“. The API is pretty straightforward in making a working animation.

So let’s take a look.

First we create an Activity that displays a grid of car images using a RecyclerView, GridLayoutManager and the corresponding Adapter. We want the adapter to display car images so we will create a CarItem class that represents it.

public class CarItem implements Parcelable{
private int drawableId;
public CarItem(int drawableId) {
    this.drawableId = drawableId;
}
public int getDrawableId() {
    return drawableId;
}
public void setDrawableId(int drawableId) {
    this.drawableId = drawableId;
}
@Override
public int describeContents() {
    return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
    dest.writeInt(this.drawableId);
}
protected CarItem(Parcel in) {
    this.drawableId = in.readInt();
}
public static final Creator<CarItem> CREATOR = new Creator<CarItem>() {
@Override
public CarItem createFromParcel(Parcel source) {
    return new CarItem(source);
}
@Override
public CarItem[] newArray(int size) {
    return new CarItem[size];
}
};

The CarItem implements the Parcelable interface since we will pass that object to the details activity. So we create an ArrayList CarItems and pass it to the CarsAdapter that we created.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
CarItem carItem1 = new CarItem(R.drawable.car1);
CarItem carItem2 = new CarItem(R.drawable.car2);
CarItem carItem3 = new CarItem(R.drawable.car3);
CarItem carItem4 = new CarItem(R.drawable.car4);
CarItem carItem5 = new CarItem(R.drawable.car5);
CarItem carItem6 = new CarItem(R.drawable.car6);
CarItem carItem7 = new CarItem(R.drawable.car7);
CarItem carItem8 = new CarItem(R.drawable.car8);
CarItem carItem9 = new CarItem(R.drawable.car9);
CarItem carItem10 = new CarItem(R.drawable.car10);
ArrayList<CarItem> carItems = new ArrayList<>();
carItems.add(carItem1);
carItems.add(carItem2);
carItems.add(carItem3);
carItems.add(carItem4);
carItems.add(carItem5);
carItems.add(carItem6);
carItems.add(carItem7);
carItems.add(carItem8);
carItems.add(carItem9);
carItems.add(carItem10);
CarsAdapter carsAdapter = new CarsAdapter(this, carItems, this);
GridLayoutManager gridLayoutManager = new GridLayoutManager(getApplicationContext(), 2);
CarsItemDecoration carsItemDecoration = new CarsItemDecoration((int)Utils.convertDpToPx(this, 10));
recCars.setAdapter(carsAdapter);
recCars.setLayoutManager(gridLayoutManager);
recCars.addItemDecoration(carsItemDecoration);

In the onCreate() of our MainActivity we created 10 CarItems objects and passed it to the adapter. We also created a GridLayout manager with a span size of 2, which means it will have 2 columns, and a CarsItemDecoration which just handles spaces between the items. So we attach everything to our RecyclerView. If we start our app now, we should have the following screen.
Activity Animations: shared element transitions demystified
So the next thing we have to do is attach a click listener to our CarsAdapter that will tell us when a grid item is clicked. When the item is clicked we have to animate the clicked image to move to the designated position in the next activity.

Let’s create the DetailsActivty so we know where our image should move. This Activity, for our purposes, needs to be really simple.

public class DetailsActivity extends AppCompatActivity {
   public static final String EXTRA_CAR_ITEM =
"com.example.sharedelementtransition.model.CarItem";
   @BindView(R.id.img_details_car) ImageView imgDetailsCar;
   @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_details);
   ButterKnife.bind(this);
   CarItem carItem = getIntent().getParcelableExtra(EXTRA_CAR_ITEM);
   imgDetailsCar.setImageResource(carItem.getDrawableId());
    }
}

The static constant EXTRA_CAR_ITEM represents out the extra name that we will pass from the MainActivity to this DetailsActivity. The extra is the parceled CartItem object that we get in the onCreate() method. We set the image to the ImageView that we defined in this Activity.

For the API to know which view it should animate we have to attach a transitionName to the view from which we want the animation to start, and the same transitionName to the view where the animation should end. Let’s do that.

The view where the animation should start is, in fact, the ImageView from the grid item in the MainActivity. So let’s add that to the cell layout.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
  <ImageView
    android:id="@+id/img_car"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:transitionName="carTransition" />
</FrameLayout>

Now we add the same android:transitionName=“carTransition“ to the DetailsActivity layout.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:gravity="center_horizontal"
     android:orientation="vertical">
  <ImageView
     android:id="@+id/img_details_car"
     android:layout_width="200dp"
     android:layout_height="200dp"
     android:transitionName="carTransition" />
</LinearLayout>

The only thing left now is define the animation in the Intent options. First we attach a click event to the ViewHolder of the  grid item.

class ViewHolder extends RecyclerView.ViewHolder {
   @BindView(R.id.img_car)
   ImageView imgCar;
public ViewHolder(View itemView) {
    super(itemView);
    ButterKnife.bind(this, itemView);
}
@OnClick(R.id.root) void onCarItemClick(View view){
    int position = getAdapterPosition();
    if(position != RecyclerView.NO_POSITION){
        CarItem carItem = carItems.get(position);
        carsAdapterListener.onCarItemClicked(view, carItem);
     }
  }
}

Now let’s start the Intent that will open the DetailsActivity in the implemented interface method in the MainActivity.

@Override
public void onCarItemClicked(View view, CarItem carItem) {
    View carView = ButterKnife.findById(view, R.id.img_car);
    Intent intent = new Intent(this, DetailsActivity.class);
    intent.putExtra(DetailsActivity.EXTRA_CAR_ITEM, carItem);
    ActivityOptionsCompat options =
ActivityOptionsCompat.makeSceneTransitionAnimation(this, carView, "carTransition");
    ActivityCompat.startActivity(this, intent, options.toBundle());
}

In the implemented method we passed the root layout of the grid item and the CarItem object.

First, we find the view we want to animate, and that’s the ImageView in the root layout. After that, we add the CarItem to the Intent extra so it gets passed to the DetailsActivity.

In the end, we define the transition animations options by passing Context, carView (The view where the animation should start and where the transitionName is defined) and „carTransition“ (The transition name).

Voila, done.

After clicking a grid item, the animation should start and animate from the grid to the ImageView in the DetailsActivity.

Pre-Lollipop

Nice and clean API for the shared element transition. But what if we wan’t to support our animations on older API. There things get a little bit more complicated and we have to better understand how those animations work.

Basically we need to display the DetailsActivity with transparent background over the MainActiviy and slowly animate the view transition and background color.
Activity Animations: shared element transitions demystified
By providing a transparent background to the root layout of the DetailsActivity we won’t achieve much since the system is drawing another layout behind our root layout.

That is the windowBackground. So we have to tell the system that our window background should be transparent.

We create a new Theme that we will attach to the DetailsActivity which has the transparent windowBackground.

We also have to set the windowIsTranslucent to true.

<style name="AppTheme.Transparent">
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
</style>

And we set that theme to the DetaislActivity in the Manifest.

<activity android:name=".DetailsActivity"
    android:theme="@style/AppTheme.Transparent"/>

Now everytime we open the DetailsActivity the MainActivity will be visible behind it.

There is one caveat here. The window manager in the system has to draw the MainActivity and the DetailsActivity to the screen so there is some performance issue here.

So the next step is to pass the view data to the DetailsActivity so that we know where the animation should start. We do that in the implemented Adapter listener method in the MainActivity.

@Override
public void onCarItemClicked(View view, CarItem carItem) {
    View carView = ButterKnife.findById(view, R.id.img_car);
    int[] screenLocation = new int[2];
    carView.getLocationOnScreen(screenLocation);
    Intent intent = new Intent(this, DetailsActivity.class);
    intent.putExtra(DetailsActivity.EXTRA_CAR_ITEM, carItem);
    intent.putExtra(DetailsActivity.EXTRA_LEFT, screenLocation[0]);
    intent.putExtra(DetailsActivity.EXTRA_TOP, screenLocation[1]);
    intent.putExtra(DetailsActivity.EXTRA_WIDTH, carView.getWidth());
    intent.putExtra(DetailsActivity.EXTRA_HEIGHT, carView.getHeight());
    startActivity(intent);
    overridePendingTransition(0, 0);
}

We get the views location on the screen so we have the top and left coordinate values of the view. We are also passing the width and height of the clicked view.

Additionally we also pass the CarItem just like in our previous example. The important thing here is to call overridePendingTransitions() to not run the standard window animation.

In the details Activity we have to extract our data and calculate the position and scale factor to know where to place the initial thumbnail image that we will animate. By attaching a predraw listener to our ImageView in the DetailsActivity we can listen to the event when we know we have to start our animation.

We also set a ColorDrawable to the root layout for which we will animate the opacity so that it looks like our activity fades in from transparent to white.

Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_details);
    ButterKnife.bind(this);
    CarItem carItem = getIntent().getParcelableExtra(EXTRA_CAR_ITEM);
    final int thumbnailLeft = getIntent().getIntExtra(EXTRA_LEFT, 0);
    final int thumbnailTop = getIntent().getIntExtra(EXTRA_TOP, 0);
    final int thumbnailWidth = getIntent().getIntExtra(EXTRA_WIDTH, 0);
    final int thumbnailHeight = getIntent().getIntExtra(EXTRA_HEIGHT, 0);
    imgDetailsCar.setImageResource(carItem.getDrawableId());
    background = new ColorDrawable(Color.WHITE);
    root.setBackground(background);
if (savedInstanceState == null) {
    ViewTreeObserver observer = imgDetailsCar.getViewTreeObserver();
    observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            imgDetailsCar.getViewTreeObserver().removeOnPreDrawListener(this);
            int[] screenLocation = new int[2];
            imgDetailsCar.getLocationOnScreen(screenLocation);
            leftDelta = thumbnailLeft - screenLocation[0];
            topDelta = thumbnailTop - screenLocation[1];
            widthScale = (float)thumbnailWidth / imgDetailsCar.getWidth();
            heightScale = (float)thumbnailHeight / imgDetailsCar.getHeight();
            runEnterAnimation();
            return true;
         }
     });
   }
}

The important thing here is that we check if the savedInstanceState is null because we want to start the animation only when we enter the DetailsActivity from another Activity.

That way we avoid the animation when we have an orientation change since we don’t want to animate anything if we are going from portrait to landscape or vice versa.

In the predDraw listener, we remove the listener since we don’t want listen to it after this, get the screen location of the ImageView in the DetailsActivity and calculate the deltaValue of the left and top coordinates.

For getting the thumbnail size we need scaleFactor or the widths and heights of the images, in that way we can scale the image from that factor value to 1 so we get the impression the image grows into the original place.

In the end, we call runEnterAnimation() and we have to return true, otherwise, it will disable rendering on this screen. Let’s take a look at the runEnterAnimation() method.

private void runEnterAnimation() {
    final long duration = (long) (ANIM_DURATION * animatorScale);
    imgDetailsCar.setPivotX(0);
    imgDetailsCar.setPivotY(0);
    imgDetailsCar.setScaleX(widthScale);
    imgDetailsCar.setScaleY(heightScale);
    imgDetailsCar.setTranslationX(leftDelta);
    imgDetailsCar.setTranslationY(topDelta);
    imgDetailsCar.animate().setDuration(duration)
       .scaleX(1).scaleY(1)
       .translationX(0).translationY(0)
       .setInterpolator(decelerator);
    ObjectAnimator bgAnimator = ObjectAnimator.ofInt(background, "alpha", 0, 255);
    bgAnimator.setDuration(duration);
    bgAnimator.start();
}

We calculate the duration and set the starting value properties we’re going to animate. These values scale and position the full-size image down to the thumbnail size/ location, from which we will animate it to the end size/location.

After that, we run a view property animation where we scale the image to the original scale 1 and to the original location (0, 0).

In parralel we are going to fade in the white background by animating the alpha value of the ColorDrawable from 0 to 255. The only thing left now is to reverse the animation when we press the back button.

First, we override the onBackPressed(), remove the super call since we don’t want to run the default behavior, and pass a Runnable to our runExitAnimation(Runnable runnable) that will call the finish() method as the end action of our animation.

We also override finish() method for the sole reason of overriding the pending transition since we don’t want to run the default transition.

@Override
public void onBackPressed() {
    runExitAnimation(new Runnable() {
        @Override
        public void run() {
            finish();
         }
    });
}
@Override
public void finish() {
    super.finish();
    overridePendingTransition(0, 0);
}

In the runEnterAnimation(Runnable runnable) we simply reverse the animation. Set the scale factor and position to the values we calculated for the thumbnail image, and run the animation with the end action calling our runnable that calls finish().

We also reverse the backgrounds ColorDrawable alpha value.

private void runExitAnimation(Runnable runnable){
   final long duration = (long) (ANIM_DURATION * animatorScale);
   imgDetailsCar.animate().setDuration(duration)
           .scaleX(widthScale).scaleY(heightScale)
           .translationX(leftDelta).translationY(topDelta)
           .withEndAction(runnable);
   ObjectAnimator bgAnimator = ObjectAnimator.ofInt(background, "alpha", 0);
   bgAnimator.setDuration(duration);
   bgAnimator.start();
}

Done. This should be enough for our Pre-Lollipop animations to work. By running the app we should see something like this:

Conclusion

As you can see, writing custom transition animations aren’t so hard when you get the hang of it, but it takes some time to make them work how you desire. Of course, the Android Shared Element Transition API is much more powerful than our custom animation.

There are so much more properties to consider, like the ImageViews ScaleType, explode effect, reveal effect, curved motion etc. Writing this manually would be a headache. The main point of this article was to learn what is going on behind the transition animations, and with some simple math, we even copied the behavior to our custom transition animation.

Now go and create your own awesome transition and happy coding!

Explore next

Related posts

We use cookies to optimize our website. By using our services, you agree to our use of cookies.