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)
Tags:
* Android Material UI/UX