Hey people! Welcome to my blog on shared element transition using fragments with navigation component.
Before we begin with the implementation it is important to understand the difference between element transition and other such as container transforms.
Container transforms opens a View from a ViewGroup into a fragment or activity as a whole. Whereas, element or shared element transition only moves the views from one place to other.
1. Add dependencies to project
Start with new android studio project choosing Empty Activity.
Give project and package name and select language Kotlin.
Minimum SDK as API 21: Lollipop and finish
gradle.build(Module: app)
// ConstraintLayout
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
// Material Design Components
implementation 'com.google.android.material:material:1.1.0'
// Navigation Component
implementation "androidx.navigation:navigation-fragment-ktx:2.3.0"
implementation "androidx.navigation:navigation-ui-ktx:2.3.0"
// RecyclerView
implementation 'androidx.recyclerview:recyclerview:1.1.0'
2. Create destination fragments
We need two fragments for shared element transition to take place. First fragment has RecyclerView list data and Second fragment has detail for each item from the list.
So create two blank fragments with names as follows and leave them empty for now:
ListFragment.kt
with fragment_list.xml
DetailFragment.kt with fragment_detail.xml
If you are unsure of this, take this as reference. BlankFragment and fragment_blank.xml
3. Make navigation graph and attach to activity
Let’s create a graph to connect all destinations in our app with actions. In our case we connect previously created empty fragments.
Right click on res -->
New -->
Android Resource File
In the dialog section:
File name -->
nav_graph
Resource type -->
Navigation and then click Ok.
Attach graph to Activity :
Let’s make our MainActivity class as default navigation host for all fragments by attaching our navigation graph.
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"
tools:ignore="FragmentTagUsage" />
</androidx.constraintlayout.widget.ConstraintLayout>
Let’s set NavController with ActionBar using NavigationUI to the activity class
MainActivity.kt:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.NavigationUI
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navController = this.findNavController(R.id.nav_host_fragment)
NavigationUI.setupActionBarWithNavController(this, navController)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp()
}
}
4. Connect destinations with actions
Open nav_graph.xml file that you have created earlier, in that file from the toolbar go to Design section.
Click the New Destination icon to see available destinations to add.
Locate fragment_list and then click to insert, similarly add fragment_detail to the graph.
Now arrange the layouts properly. Select listFragment and drag the connection to detailFragment.
Here’s the xml layout for what we did.
nav_graph.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/listFragment">
<fragment
android:id="@+id/listFragment"
android:name="com.developersbreach.myapplication.ListFragment"
android:label="List"
tools:layout="@layout/fragment_list">
<action
android:id="@+id/listToDetailFragment"
app:destination="@id/detailFragment" />
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.developersbreach.myapplication.DetailFragment"
android:label="Detail"
tools:layout="@layout/fragment_detail" />
</navigation>
Now run the app and you will be able to see blank screen with no views to make sure everything is working.
5. Create classes to show RecyclerView and data
We need data such as text string resources to show in layout, let’s copy the strings for showing titles and other data, colors, drawables and styles for the app.
Get all the strings and paste into your strings.xml file.
Get all the colors and paste into your colors.xml file.
Get all the styles and paste into your styles.xml file.
Get all drawables and paste into your drawables folder.
Now let’s create a data model class Sports.kt which helps in making a list.
Sports.kt:
import android.content.Context
import android.os.Parcelable
import com.developersbreach.sharedelementtransitionexample.R
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Sports(
val id: Int,
val banner: Int,
val title: String,
val about: String
) :
Parcelable {
companion object {
fun sportsList(context: Context): List<Sports> {
return listOf(
Sports(0, R.drawable.ic_rugby,
context.getString(R.string.title_rugby),
context.getString(R.string.about_rugby)
),
Sports(
1, R.drawable.ic_cricket,
context.getString(R.string.title_cricket),
context.getString(R.string.about_cricket)
),
Sports(
2, R.drawable.ic_hockey,
context.getString(R.string.title_hockey),
context.getString(R.string.about_hockey)
),
Sports(3, R.drawable.ic_basketball,
context.getString(R.string.title_basketball),
context.getString(R.string.about_basketball)
),
Sports(4, R.drawable.ic_volleyball,
context.getString(R.string.title_volleyball),
context.getString(R.string.about_volleyball)
),
Sports(5, R.drawable.ic_esports,
context.getString(R.string.title_esports),
context.getString(R.string.about_esports)
),
Sports(6, R.drawable.ic_kabaddi,
context.getString(R.string.title_kabbadi),
context.getString(R.string.about_kabbadi)
),
Sports(7, R.drawable.ic_baseball,
context.getString(R.string.title_baseball),
context.getString(R.string.about_baseball)
),
Sports(8, R.drawable.ic_mma,
context.getString(R.string.title_mma),
context.getString(R.string.about_mma)
),
Sports(9, R.drawable.ic_soccer,
context.getString(R.string.title_soccer),
context.getString(R.string.about_soccer)
),
Sports(10, R.drawable.ic_handball,
context.getString(R.string.title_handball),
context.getString(R.string.about_handball)
),
Sports(11, R.drawable.ic_tennis,
context.getString(R.string.title_tennis),
context.getString(R.string.about_tennis)
)
)
}
}
}
Create item_sports layout file to show each item in list.
item_sports.xml:
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp"
app:cardElevation="6dp"
app:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/sports_item_image_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="16dp"
android:scaleType="fitCenter"
app:layout_constraintDimensionRatio="2:2"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_hockey" />
<TextView
android:id="@+id/title_item_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
android:textColor="?attr/colorOnSurface"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sports_item_image_view"
tools:text="Title" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
Finally create an RecyclerView adapter for fragment to show a list.
SportsAdapter.kt:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class SportsAdapter(
private val sportsList: List<Sports>,
private val onClickListener: OnClickListener
) :
RecyclerView.Adapter<SportsAdapter.SportsViewHolder>() {
class SportsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val banner: ImageView = itemView.findViewById(R.id.sports_item_image_view)
private val title: TextView = itemView.findViewById(R.id.title_item_text_view)
fun bind(
sports: Sports,
onClickListener: OnClickListener
) {
banner.setImageResource(sports.banner)
title.text = sports.title
itemView.setOnClickListener {
onClickListener.onClick(sports)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SportsViewHolder {
return SportsViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_sports,
parent,
false
)
)
}
override fun onBindViewHolder(holder: SportsViewHolder, position: Int) {
val sports: Sports = sportsList[position]
holder.bind(sports, onClickListener)
}
override fun getItemCount() = sportsList.size
class OnClickListener(val clickListener: (Sports) -> Unit) {
fun onClick(
sports: Sports
) = clickListener(sports)
}
}
6. With SafeArgs pass data class as argument
We have to pass our data class Sports.kt as argument from ListFragment to DetailFragment, and we will do this from nav_graph file.
In nav_graph.xml file go to design section from toolbar.
Select detailFragment, to the right side Attributes section, click plus(+) button to add new arguments to that fragment.
In Add Arguments section dialog :
Name -->
sportsArgs
Type -->
Custom Parcelable -->
Sports.kt
Continue by clicking OK and then again select Add to finish.
Now Rebuild the project for android studio to auto generate the classes required for this.
Note: If build is not successful or android studio cannot build at this point, try cleaning the project and Rebuilding it from
Build-->
Clean Project.
7. Show list and detail data in Fragments
Now we have data and arguments ready to be passed let’s populate the data inside fragments.
Add RecyclerView to ListFragment.kt and layout. Replace the code inside the layout with below one.
fragment_list.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/sports_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:spanCount="2"
tools:itemCount="12"
tools:listitem="@layout/item_sports" />
</androidx.constraintlayout.widget.ConstraintLayout>
ListFragment.kt:
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import com.developersbreach.sharedelementtransitionexample.R
class ListFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.sports_recycler_view)
val list = Sports.sportsList(requireContext())
val adapter = SportsAdapter(list, sportsItemListener)
recyclerView.adapter = adapter
}
private val sportsItemListener = SportsAdapter.OnClickListener { sports ->
val direction: NavDirections =
ListFragmentDirections.listToDetailFragment(sports)
findNavController().navigate(direction)
}
}
Our ListFragment is ready to show the RecyclerView, let’s complete DetailFragment to show each list item which user is selected.
fragment_detail.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".DetailFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/detail_image_view"
android:layout_width="match_parent"
android:layout_height="256dp"
android:scaleType="fitCenter"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription"
tools:src="@drawable/ic_hockey" />
<TextView
android:id="@+id/title_detail_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
android:textColor="?attr/colorOnPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/detail_image_view"
tools:text="Title" />
<TextView
android:id="@+id/about_detail_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="28dp"
android:layout_marginEnd="16dp"
android:justificationMode="inter_word"
android:textColor="?attr/colorOnPrimary"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_detail_text_view"
tools:targetApi="o"
tools:text="@string/about_tennis" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
FragmentDetail.kt:
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
class DetailFragment : Fragment() {
private lateinit var sportsArgs: Sports
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val args = requireArguments()
sportsArgs = DetailFragmentArgs.fromBundle(args).sportsArgs
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_detail, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val banner: ImageView = view.findViewById(R.id.detail_image_view)
val title: TextView = view.findViewById(R.id.title_detail_text_view)
val about: TextView = view.findViewById(R.id.about_detail_text_view)
banner.setImageResource(sportsArgs.banner)
title.text = sportsArgs.title
about.text = sportsArgs.about
}
}
Now run the app, ListFragment will be able to show list of Cards and when you click single item you will be navigated to DetailFragment with no errors.
Now let’s see how we can add shared element transitions between these fragments.
8. Make SharedElementTransition between fragments
To perform any transition between views first we need to identity those views from both layouts. So in our case we are performing transition between two layouts which are item_sports.xml to fragment_detail.xml. See the image below.

Since our item views are created by the adapter let’s make changes to those. Open SportsAdapter.kt file and scroll down below to class which implements OnClickListener, this takes single argument which is data class but now change that to take two more arguments which are ImageView(banner) and TextView(title).
These are the changes you need to make:
class OnClickListener(val clickListener: (Sports, ImageView, TextView) -> Unit) {
fun onClick(
sports: Sports,
banner: ImageView,
title: TextView
) = clickListener(sports, banner, title)
}
Now for views which we are passing needs unique transition names while transition occurs, if all views have same transition name then transition won’t take place.
So the best way to do this is by giving each view transition name from it’s data class itself which must be of the type String.
We have access to both banner, title view with data in SportsViewHolder class.
These are the changes you need to make:
Inside SportsViewHolder
fun bind(
sports: Sports,
onClickListener: OnClickListener
) {
banner.setImageResource(sports.banner)
title.text = sports.title
banner.transitionName = sports.banner.toString()
title.transitionName = sports.title
itemView.setOnClickListener {
onClickListener.onClick(sports, banner, title)
}
}
Since ListFragment.kt implements OnClickListener it takes extra parameters after changes which needs ImageView and TextView, so we will pass that which shown below.
Let’s use FragmentNavigatorExtras which builds pair of view with string value. We can pass as many as pairs we want for transition to take place. In our case we are going to pass two pairs a ImageView for banner and TextView for title.
After with an assigned value we pass that inside navigate along with directions and extras.
After all the required changes this is how your code should look like.
ListFragment.kt:
private val sportsItemListener = SportsAdapter.OnClickListener { sports, imageView, textView ->
val direction: NavDirections =
ListFragmentDirections.listToDetailFragment(sports)
val extras = FragmentNavigatorExtras(
imageView to sports.banner.toString(),
textView to sports.title
)
findNavController().navigate(direction, extras)
}
We have to specify what kind of transition should take place from the leaving fragment and for that we have inflate a transition type using TransitionInflater in our fragment OnCreateView before returning.
So let’s inflate the transition with type move effect, see those changes below.
onCreateView ListFragment
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_list, container, false)
sharedElementReturnTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
return view
}
With this we will be able to perform moving transition but when user clicks back button we must be able to perform return transition, so we will this changes inside OnViewCreated after all views have been ready.
This gives fragment the ability to delay fragment animations until all data is being loaded.
With these changes you can get backward transition taking place.
onViewCreated at the end
postponeEnterTransition()
But enter transition on any view should happen while it’s been drawn. So kotlin has a way of doing this by calling doOnPreDraw for view for transition.
In our case we have to call this on RecyclerView while being drawn or laid out.
onViewCreated after enter transition
recyclerView.doOnPreDraw { startPostponedEnterTransition() }
If you get an error at doOnPreDraw which means you have to specify proper jvm target version. So here’s the fix for it.
build.gradle(Module:app) after build types
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
Now Sync the project to fix this.
With all the changes being made to ListFragment. This is how it should look like.
ListFragment.kt:
class ListFragment : Fragment() {
private lateinit var recyclerView: RecyclerView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_list, container, false)
sharedElementReturnTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = view.findViewById(R.id.sports_recycler_view)
val list = Sports.sportsList(requireContext())
val adapter = SportsAdapter(list, sportsItemListener)
recyclerView.adapter = adapter
// When user hits back button transition takes backward
postponeEnterTransition()
recyclerView.doOnPreDraw {
startPostponedEnterTransition()
}
}
private val sportsItemListener = SportsAdapter.OnClickListener { sports, imageView, textView ->
val direction: NavDirections =
ListFragmentDirections.listToDetailFragment(sports)
val extras = FragmentNavigatorExtras(
imageView to sports.banner.toString(),
textView to sports.title
)
findNavController().navigate(direction, extras)
}
}
One last thing remaining to do is to apply these transition names in DetailFragment for transition to occur correctly.
Just like ListFragment one same thing we have to do is to inflate transition same way.
Optionally you can delay the transition from leaving fragment at certain duration and let the transition take place.
onCreateView
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
postponeEnterTransition(250, TimeUnit.MILLISECONDS)
DetailsFragment.kt:
class DetailFragment : Fragment() {
private lateinit var sportsArgs: Sports
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val args = requireArguments()
sportsArgs = DetailFragmentArgs.fromBundle(args).modelArgs
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_detail, container, false)
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
postponeEnterTransition(250, TimeUnit.MILLISECONDS)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val banner: ImageView = view.findViewById(R.id.detail_image_view)
val title: TextView = view.findViewById(R.id.title_detail_text_view)
val about: TextView = view.findViewById(R.id.about_detail_text_view)
banner.setImageResource(sportsArgs.banner)
title.text = sportsArgs.title
about.text = sportsArgs.about
banner.transitionName = sportsArgs.banner.toString()
title.transitionName = sportsArgs.title
}
}
Now run the app check how transitions takes place between them. For your better understanding experiment by removing code where transition changes or doesn’t work at any point.
You will find this example wrote completely with BindingAdapters in my GitHub repository in different branch, so make sure to check that too.
9. External links, project Github and download
- Android Developer Documentation – Animate transition between destinations
- Learn about Motion ContainerTransform, SharedAxis, Fade – Material Component

Rajasekhar K E
Hi ! I’m Rajasekhar a Programmer who does Android Development, Creative & Technical writing, Kotlin enthusiast and Engineering graduate. I learn from Open Source and always happy to assist others with my work. I spend most of time Training, Assisting & Mentoring students who are absolute Beginners in android development. I’m also running my startup named Developers Breach which mostly works on contributing to open source.
Here We Go Again : (
if (article == helpful) {
println("Like and subscribe to blog newsletter.")
} else {
println("Let me know what i should blog on.")
}
You must be logged in to post a comment.