Shared Element Transitions: Kotlin XML Guide for Android Development

Shared element transitions provide a seamless visual connection between different activities in an Android app. They enhance the user experience by animating a view or a set of views from one activity to another during a transition. While Jetpack Compose is the modern approach to Android UI, many apps still utilize XML layouts, making it essential to understand shared element transitions in this context. This article explores how to implement shared element transitions between activities using Kotlin and XML layouts in Android development.

What are Shared Element Transitions?

Shared element transitions animate a shared view or a set of views between two activities, creating a visual bridge that makes the app feel more cohesive. These transitions are especially useful when navigating between screens that display related content.

Why Use Shared Element Transitions?

  • Improved User Experience: Provides a smooth and natural transition between activities.
  • Enhanced Visual Continuity: Connects related content visually, making navigation intuitive.
  • Modern UI Design: Aligns with modern UI trends, providing a polished and professional feel.

How to Implement Shared Element Transitions in Kotlin XML Development

To implement shared element transitions, follow these steps:

Step 1: Set up Transition Names in XML Layouts

Define a unique android:transitionName for the shared elements in both the source and target XML layouts.

Source Activity (activity_main.xml):

<ImageView
    android:id="@+id/sharedImageView"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:src="@drawable/sample_image"
    android:transitionName="sharedImageTransition"
    />
<TextView
    android:id="@+id/sharedTextView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Shared Text"
    android:transitionName="sharedTextTransition"
    />

Target Activity (activity_detail.xml):

<ImageView
    android:id="@+id/detailImageView"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:src="@drawable/sample_image"
    android:transitionName="sharedImageTransition"
    />
<TextView
    android:id="@+id/detailTextView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Shared Text in Detail"
    android:transitionName="sharedTextTransition"
    />

Ensure the transitionName is the same for the corresponding shared elements in both layouts.

Step 2: Start the Target Activity with Shared Element Transitions

Use the ActivityOptionsCompat class to specify the shared elements and start the target activity.

Source Activity (MainActivity.kt):

import android.content.Intent
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val sharedImageView: ImageView = findViewById(R.id.sharedImageView)
        val sharedTextView: TextView = findViewById(R.id.sharedTextView)

        sharedImageView.setOnClickListener {
            val intent = Intent(this, DetailActivity::class.java)

            val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                this,
                Pair.create(sharedImageView, "sharedImageTransition"),
                Pair.create(sharedTextView, "sharedTextTransition")
            )

            startActivity(intent, options.toBundle())
        }
    }
}

In this code:

  • ActivityOptionsCompat.makeSceneTransitionAnimation creates an animation that transitions the specified shared elements to the new activity.
  • Pair.create(view, transitionName) creates pairs of shared views and their transition names.

Step 3: Configure Target Activity to Support Transitions

In the target activity, enable the window content transitions to allow shared element transitions.

Target Activity (DetailActivity.kt):

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.transition.TransitionInflater

class DetailActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)

        // Enable window content transitions
        window.sharedElementEnterTransition = TransitionInflater.from(this)
            .inflateTransition(android.R.transition.move)
    }
}

In the onCreate method, window.sharedElementEnterTransition is set to android.R.transition.move, a default transition that moves shared elements smoothly.

Step 4: Add Transitions to Styles

In your styles.xml file, set the windowContentTransitions attribute to true for your activity theme.

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:windowContentTransitions">true</item>
</style>

Step 5: Enable Postpone Enter Transition (Optional)

In some cases, you might need to load data or images in the target activity before starting the transition. You can postpone the enter transition and start it manually once the necessary data is loaded.

Target Activity (DetailActivity.kt):

import android.os.Bundle
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.transition.TransitionInflater
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import java.lang.Exception

class DetailActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)

        // Enable window content transitions
        window.sharedElementEnterTransition = TransitionInflater.from(this)
            .inflateTransition(android.R.transition.move)

        // Postpone the shared element enter transition
        postponeEnterTransition()

        val detailImageView: ImageView = findViewById(R.id.detailImageView)

        // Load image using Picasso (or any other image loading library)
        Picasso.get()
            .load("URL_TO_YOUR_IMAGE")
            .into(detailImageView, object : Callback {
                override fun onSuccess() {
                    // Start the postponed transition
                    startPostponedEnterTransition()
                }

                override fun onError(e: Exception?) {
                    // Start the postponed transition even on error
                    startPostponedEnterTransition()
                }
            })
    }
}

Explanation:

  • postponeEnterTransition() postpones the transition until startPostponedEnterTransition() is called.
  • Load the image using an image loading library like Picasso.
  • In the onSuccess callback, call startPostponedEnterTransition() to begin the transition after the image is loaded.
  • Handle potential errors and ensure startPostponedEnterTransition() is called even if the image fails to load.

Complete Example

Here’s a comprehensive example that incorporates the key steps:

MainActivity.kt:


import android.content.Intent
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val sharedImageView: ImageView = findViewById(R.id.sharedImageView)
        val sharedTextView: TextView = findViewById(R.id.sharedTextView)

        sharedImageView.setOnClickListener {
            val intent = Intent(this, DetailActivity::class.java)

            val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
                this,
                Pair.create(sharedImageView, "sharedImageTransition"),
                Pair.create(sharedTextView, "sharedTextTransition")
            )

            startActivity(intent, options.toBundle())
        }
    }
}

activity_main.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <ImageView
        android:id="@+id/sharedImageView"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/sample_image"
        android:transitionName="sharedImageTransition" />

    <TextView
        android:id="@+id/sharedTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Shared Text"
        android:transitionName="sharedTextTransition" />

</LinearLayout>

DetailActivity.kt:


import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.transition.TransitionInflater

class DetailActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)

        // Enable window content transitions
        window.sharedElementEnterTransition = TransitionInflater.from(this)
            .inflateTransition(android.R.transition.move)
    }
}

activity_detail.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <ImageView
        android:id="@+id/detailImageView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:src="@drawable/sample_image"
        android:transitionName="sharedImageTransition" />

    <TextView
        android:id="@+id/detailTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Shared Text in Detail"
        android:transitionName="sharedTextTransition" />

</LinearLayout>

styles.xml:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    <item name="android:windowContentTransitions">true</item>
</style>

Best Practices

  • Use Unique Transition Names: Ensure each shared element has a unique transitionName to avoid conflicts.
  • Optimize Image Loading: Use image loading libraries like Picasso or Glide to efficiently load images and prevent delays in the transition.
  • Handle Postponed Transitions Carefully: Ensure startPostponedEnterTransition() is always called, even if data loading fails, to prevent the activity from being stuck.
  • Test on Multiple Devices: Test shared element transitions on different devices and screen sizes to ensure a consistent user experience.

Conclusion

Shared element transitions provide a polished and intuitive navigation experience in Android applications. By setting up transition names, using ActivityOptionsCompat, and enabling window content transitions, developers can create seamless animations between activities. Whether you are building a new app or maintaining an existing one, understanding shared element transitions is essential for providing a modern and engaging user interface.