Search code examples
androidandroid-recyclerviewshared-element-transitionandroid-architecture-navigation

How to implement shared transition element from RecyclerView item to Fragment with Android Navigation Component?


I have a pretty straightforward case. I want to implement shared element transition between an item in recyclerView and fragment. I'm using android navigation component in my app.

There is an article about shared transition on developer.android and topic on stackoverflow but this solution works only for view that located in fragment layout that starts transition and doesn't work for items from RecyclerView. Also there is a lib on github but i don't want to rely on 3rd party libs and do it by myself.

Is there some solution for this? Maybe it should work and this is just a bug? But I haven't found any information about it.

code sample:

transition start

class TransitionStartFragment: Fragment() {

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.fragment_transition_start, container, false)
    }

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val testData = listOf("one", "two", "three")
    val adapter = TestAdapter(testData, View.OnClickListener { transitionWithTextViewInRecyclerViewItem(it) })
    val recyclerView = view.findViewById<RecyclerView>(R.id.test_list)
    recyclerView.adapter = adapter
    val button = view.findViewById<Button>(R.id.open_transition_end_fragment)
    button.setOnClickListener { transitionWithTextViewInFragment() }
    }

private fun transitionWithTextViewInFragment(){
    val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
    val extras = FragmentNavigatorExtras(transition_start_text to "transitionTextEnd")
    findNavController().navigate(destination, extras)
    }

private fun transitionWithTextViewInRecyclerViewItem(view: View){
    val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
    val extras = FragmentNavigatorExtras(view to "transitionTextEnd")
    findNavController().navigate(destination, extras)
   }

}

layout

<androidx.constraintlayout.widget.ConstraintLayout 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:orientation="vertical">

<TextView
    android:id="@+id/transition_start_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="transition"
    android:transitionName="transitionTextStart"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<Button
    android:id="@+id/open_transition_end_fragment"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toBottomOf="@id/transition_start_text"
    android:text="open transition end fragment" />

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/test_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toBottomOf="@id/open_transition_end_fragment"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

adapter for recyclerView

class TestAdapter(
    private val items: List<String>,
    private val onItemClickListener: View.OnClickListener
) : RecyclerView.Adapter<TestAdapter.ViewHodler>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHodler {
    return ViewHodler(LayoutInflater.from(parent.context).inflate(R.layout.item_test, parent, false))
    }

override fun getItemCount(): Int {
    return items.size
    }

override fun onBindViewHolder(holder: ViewHodler, position: Int) {
    val item = items[position]
    holder.transitionText.text = item
    holder.itemView.setOnClickListener { onItemClickListener.onClick(holder.transitionText) }

    }

class ViewHodler(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val transitionText = itemView.findViewById<TextView>(R.id.item_test_text)
    }
}

in onItemClick I pass the textView form item in recyclerView for transition

transition end

class TransitionEndFragment : Fragment() {

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    setUpTransition()
    return inflater.inflate(R.layout.fragment_transition_end, container, false)
    }

private fun setUpTransition(){
    sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)

    }
}

layout

<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"
android:orientation="vertical">

<TextView
    android:id="@+id/transition_end_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="transition"
    android:transitionName="transitionTextEnd"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

fun transitionWithTextViewInFragment() - has transition.

fun transitionWithTextViewInRecyclerViewItem(view: View) - no transition.


Solution

  • To solve the return transition problem you need to add this lines on the Source Fragment (the fragment with the recycler view) where you initialize your recycler view

    // your recyclerView
    recyclerView.apply {
                    ...
                    adapter = myAdapter
                    postponeEnterTransition()
                    viewTreeObserver
                        .addOnPreDrawListener {
                            startPostponedEnterTransition()
                            true
                        }
    }