RecyclerView Navigation transitions in Android Navigation Architecture Component

Navigational transitions occur when users move between screens, such as from a home screen to a detail screen.

Navigation transitions use motion to guide users between two screens in your app. They help users orient themselves by expressing your app's hierarchy, using movement to indicate how elements are related to one another.

For example, when an element expands to fill the entire screen, the act of expansion expresses that the new screen is a child element. The screen from which it expanded is its parent element.

To implement transition element from RecyclerView item to Fragment with Android Navigation Component, follow this code

In our Home fragment xml layout, we need to add recycler view widget with 'transition group' should be true

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView 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/cardsRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    android:transitionGroup="true"

    android:background="#e9ecef"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
    tools:listitem="@layout/recycler_view_design">
</androidx.recyclerview.widget.RecyclerView>
Sets whether or not this ViewGroup should be treated as a single entity when doing an Activity transition. Typically, the elements inside a ViewGroup are each transitioned from the scene individually. The default for a ViewGroup is false unless it has a background
..
Then we have created Home fragment with recycler view functionality

Fragment the ability to delay Fragment animations until all data is loaded.
 postponeEnterTransition()
        view.doOnPreDraw { startPostponedEnterTransition() }
Sets the Transition that will be used to move Views out of the scene when the fragment is removed, hidden, or detached when not popping the back stack.
 exitTransition = MaterialElevationScale(false).apply {
                        duration = 100
                    }
                    reenterTransition = MaterialElevationScale(true).apply {
                        duration = 100
                    }

transitionName – The name of the View to uniquely identify it for Transitions.
 bindingDesign.cardView.apply {
                transitionName = "Title $position"
            }
..
Here is my HomeFragment code with functionality (Fragment, Adapter & Data class)
// Fragment
class RecyclerViewFragment : BaseFragment<RecyclerViewBinding>() {

    private var recyclerItemModels  =   ArrayList<MyModel>()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        postponeEnterTransition()
        view.doOnPreDraw { startPostponedEnterTransition() }


        arrayList()
        binding.cardsRecyclerView.apply {
            layoutManager      = LinearLayoutManager(context)
            hasFixedSize()
            binding.cardsRecyclerView.adapter = DashboardAdapter(event = { rowEvenBinding ->

                if (findNavController().currentDestination!!.id == R.id.nav_recycler_view) {
                    val extras: FragmentNavigator.Extras = FragmentNavigatorExtras(
                        rowEvenBinding.cardView to rowEvenBinding.cardView.transitionName
                    )
                    findNavController().navigate(
                        RecyclerViewFragmentDirections.actionNavRecyclerViewToNavDetailView(rowEvenBinding.cardView.transitionName,""+rowEvenBinding.titleText.text),
                        extras
                    )

                    exitTransition = MaterialElevationScale(false).apply {
                        duration = 100
                    }
                    reenterTransition = MaterialElevationScale(true).apply {
                        duration = 100
                    }
                }
            },recyclerItemModels)
        }
    }

    private fun arrayList() {
        recyclerItemModels.clear()
        recyclerItemModels.add(MyModel("App bars: bottom"))
        recyclerItemModels.add(MyModel("App bars: top"))
        recyclerItemModels.add(MyModel("Bottom navigation"))
        recyclerItemModels.add(MyModel("Buttons"))
        recyclerItemModels.add(MyModel("Buttons: floating action button"))
        recyclerItemModels.add(MyModel("Cards"))
        recyclerItemModels.add(MyModel("Chips"))
        recyclerItemModels.add(MyModel("Dialogs"))
        recyclerItemModels.add(MyModel("Menus"))
        recyclerItemModels.add(MyModel("Navigation drawer"))
        recyclerItemModels.add(MyModel("Date Pickers"))
        recyclerItemModels.add(MyModel("Time Pickers"))
        recyclerItemModels.add(MyModel("Progress indicators"))
        recyclerItemModels.add(MyModel("Radio buttons"))
        recyclerItemModels.add(MyModel("Checkboxes"))
        recyclerItemModels.add(MyModel("Switches"))
        recyclerItemModels.add(MyModel("Sheets: bottom"))
        recyclerItemModels.add(MyModel("Sliders"))
        recyclerItemModels.add(MyModel("Snackbars"))
        recyclerItemModels.add(MyModel("Tabs"))
        recyclerItemModels.add(MyModel("Text fields"))
        recyclerItemModels.add(MyModel("Shapeable Image View"))
        recyclerItemModels.add(MyModel("Font"))
        recyclerItemModels.add(MyModel("Elevation & Shadow"))
        recyclerItemModels.add(MyModel("Shape Theming"))
        recyclerItemModels.add(MyModel("Transition"))
        recyclerItemModels.add(MyModel("Color"))
        recyclerItemModels.add(MyModel("Data tables"))
        recyclerItemModels.add(MyModel("Dividers"))
        recyclerItemModels.add(MyModel("Image lists"))
        recyclerItemModels.add(MyModel("Navigation rail"))
        recyclerItemModels.add(MyModel("Sheets: side"))
        recyclerItemModels.add(MyModel("Lists"))
        recyclerItemModels.add(MyModel("Backdrop"))
        recyclerItemModels.add(MyModel("Banners"))
        recyclerItemModels.add(MyModel("Tooltips"))
    }

    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?) =
        RecyclerViewBinding.inflate(inflater, container, false)
}

// Adapter
class DashboardAdapterViewHolder(val bindingDesign: RecyclerViewDesignBinding) : RecyclerView.ViewHolder(bindingDesign.root)
class DashboardAdapter(
    private val event: (RecyclerViewDesignBinding) -> Unit,
    private val recyclerViewViewBindModelList: List<MyModel>) :
    RecyclerView.Adapter<DashboardAdapterViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DashboardAdapterViewHolder {
        return DashboardAdapterViewHolder(
            RecyclerViewDesignBinding.inflate(
                LayoutInflater.from(parent.context),
                parent,
                false
            )
        )
    }
    override fun onBindViewHolder(holder: DashboardAdapterViewHolder, position: Int) {
        val currentItem = recyclerViewViewBindModelList[position]
        with(holder) {


            bindingDesign.cardView.apply {
                transitionName = "Title $position"
            }

            //bindingDesign.titleText.text = currentItem.label
            bindingDesign.titleText.text = "Title $position"

        }

        holder.itemView.setOnClickListener {
            event(holder.bindingDesign)
        }
    }

    // Return the size of your data set (invoked by the layout manager)
    override fun getItemCount(): Int {
        return recyclerViewViewBindModelList.size
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getItemViewType(position: Int): Int {
        return position
    }

}


// Data class
data class MyModel(val label: String)
..
Here is my Item desgin layout page code:
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
    app:strokeWidth="2dp"
    android:id="@+id/cardView"
    app:cardElevation="10dp"
    app:cardCornerRadius="10dp"
    android:layout_margin="10dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <!--    app:strokeColor="@color/stroke_color"
-->

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <!-- Media -->
        <ImageView
            android:visibility="gone"
            android:padding="30dp"
            android:id="@+id/heroImage"
            android:layout_width="match_parent"
            android:layout_height="160dp"
            app:srcCompat="@drawable/donut"
            android:contentDescription="Image Description here"
            />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="16dp">

            <!-- Title, secondary and supporting text -->
            <TextView
                android:id="@+id/titleText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Title"
                android:textStyle="bold"
                android:textAppearance="?attr/textAppearanceHeadline6"
                />
            <TextView
                android:visibility="gone"
                android:id="@+id/subtitle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="Secondary text"
                android:textAppearance="?attr/textAppearanceBody2"
                android:textColor="?android:attr/textColorSecondary"
                />
            <TextView
                android:id="@+id/textView3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:text="Material is a design system – backed by open-source code "
                android:textAppearance="?attr/textAppearanceBody2"
                android:textColor="?android:attr/textColorSecondary"
                />

        </LinearLayout>

        <!-- Buttons -->
<!--        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:orientation="horizontal">
            <com.google.android.material.button.MaterialButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                android:text="Action 1"
                style="?attr/borderlessButtonStyle"
                />
            <com.google.android.material.button.MaterialButton
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Action 2"
                style="?attr/borderlessButtonStyle"
                />
        </LinearLayout>-->

    </LinearLayout>


</com.google.android.material.card.MaterialCardView>
..
Navigation graph with destination flow
<?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/nav_recycler_view">

    <!--this is our start destination-->
    <fragment
        android:id="@+id/nav_recycler_view"
        android:name="com.androidx.androidjetpack.RecyclerViewFragment"
        android:label="Home Fragment"
        tools:layout="@layout/recycler_view">

        <!--Navigate home to detail page-->
        <action
            android:id="@+id/action_nav_recycler_view_to_nav_detail_view"
            app:destination="@id/nav_detail_view" />
    </fragment>
    <fragment
        android:id="@+id/nav_detail_view"
        android:name="com.androidx.androidjetpack.DetailFragment"
        android:label="Detail Fragment"
        tools:layout="@layout/detail_fragment"
        >

        <!--Get this argument data from home fragment-->
        <argument
            android:name="transition"
            app:argType="string" />
        <argument
            android:defaultValue="Title"
            android:name="title"
            app:argType="string" />
    </fragment>

</navigation>
..
In our Detail Fragment
class DetailFragment : BaseFragment<DetailFragmentBinding>() {

    private val args: DetailFragmentArgs by navArgs()

    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?) =
        DetailFragmentBinding.inflate(inflater, container, false)

    @SuppressLint("RestrictedApi")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val transformation = MaterialContainerTransform()
        transformation.interpolator = AnimationUtils.LINEAR_INTERPOLATOR
        sharedElementEnterTransition = MaterialContainerTransform().apply {
            drawingViewId = R.id.nav_host_fragment_content_main
            duration = 400
            scrimColor = Color.TRANSPARENT
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val key = args.transition  // get from home fragment
        binding.titleText.text = args.title

        with(binding.cardView) {
            transitionName = key // name of the View to uniquely identify it for Transitions. 
        }
    }
}
..
Here is detail fragment layout UI design
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#e9ecef">

    <com.google.android.material.card.MaterialCardView
        android:id="@+id/cardView"
        app:cardCornerRadius="10dp"
        app:cardElevation="10dp"
        android:layout_margin="15dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">
            <!-- Media -->
            <ImageView
                android:padding="40dp"
                android:id="@+id/heroImage"
                android:layout_width="match_parent"
                android:layout_height="300dp"
                app:srcCompat="@drawable/donut"
                android:contentDescription="Image Description here"
                />
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="16dp">

                <!-- Title, secondary and supporting text -->
                <TextView
                    android:id="@+id/titleText"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Title"
                    android:textStyle="bold"
                    android:textAppearance="?attr/textAppearanceHeadline6"
                    />
                <TextView
                    android:id="@+id/subtitle"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:text="Secondary text"
                    android:textAppearance="?attr/textAppearanceBody2"
                    android:textColor="?android:attr/textColorSecondary"
                    />
                <TextView
                    android:id="@+id/textView3"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="16dp"
                    android:text="Material is a design system – backed by open-source code – that helps teams build high-quality digital experiences.


\nIn this Session, you learn about the latest design improvements to help you build personal dynamic experiences with Material Design.
"
                    android:textAppearance="?attr/textAppearanceBody2"
                    android:textColor="?android:attr/textColorSecondary"
                    />
            </LinearLayout>
        </LinearLayout>
    </com.google.android.material.card.MaterialCardView>
</FrameLayout>
..
Setting up base Fragment Class with ViewBinding

abstract class BaseFragment<VB : ViewBinding> : Fragment() {

    private var _binding: VB? = null
    protected val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = getBinding(inflater, container, savedInstanceState)
        return binding.root
    }

    protected abstract fun getBinding(
        inflater: LayoutInflater,
        container: ViewGroup?, bundle: Bundle?
    ): VB

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }


}
..
Do n't forget to both system & app level gradle (navigation plugins)

GET source code on Github:

Comments