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

前端 未结 5 1528
梦如初夏
梦如初夏 2020-12-30 06:38

I have a pretty straightforward case. I want to implement shared element transition between an item in recyclerView and fragment. I\'m using androi

相关标签:
5条回答
  • 2020-12-30 07:04

    Faced the same issue as many on SO with the return transition but for me the root cause of the problem was that Navigation currently only uses replace for fragment transactions and it caused my recycler in the start fragment to reload every time you hit back which was a problem by itself.

    So by solving the second (root) problem the return transition started to work without delayed animations. For those of you who are looking to keep the initial state when hitting back here is what I did :

    just adding a simple check in onCreateView as so

    private lateinit var binding: FragmentSearchResultsBinding
    
    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            return if (::binding.isInitialized) {
                binding.root
            } else {
                binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)
    
                with(binding) {
                    //doing some stuff here
                    root
                }
            }
    

    So triple win here: recycler is not redrawn, no refetching from server and also return transitions are working as expected.

    0 讨论(0)
  • 2020-12-30 07:09

    I have managed return transitions to work.

    Actually this is not a bug in Android and not a problem with setReorderingAllowed = true. What happens here is the original fragment (to which we return) trying to start transition before its views/data are settled up.

    To fix this we have to use postponeEnterTransition() and startPostponedEnterTransition().

    For example: Original fragment:

    class FragmentOne : Fragment(R.layout.f1) {
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            postponeEnterTransition()
    
            val items = listOf("one", "two", "three", "four", "five")
                .zip(listOf(Color.RED, Color.GRAY, Color.GREEN, Color.BLUE, Color.YELLOW))
                .map { Item(it.first, it.second) }
    
            val rv = view.findViewById<RecyclerView>(R.id.rvItems)
            rv.adapter = ItemsAdapter(items) { item, view -> navigateOn(item, view) }
    
            view.doOnPreDraw { startPostponedEnterTransition() }
        }
    
        private fun navigateOn(item: Item, view: View) {
            val extras = FragmentNavigatorExtras(view to "yura")
            findNavController().navigate(FragmentOneDirections.toTwo(item), extras)
        }
    }
    

    Next fragment:

    class FragmentTwo : Fragment(R.layout.f2) {
    
        val item: Item by lazy { arguments?.getSerializable("item") as Item }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
    
            sharedElementEnterTransition =
                TransitionInflater.from(context).inflateTransition(android.R.transition.move)
    
            val tv = view.findViewById<TextView>(R.id.tvItemId)
            with(tv) {
                text = item.id
                transitionName = "yura"
                setBackgroundColor(item.color)
            }
        }
    
    }
    

    For more details and deeper explanation see: https://issuetracker.google.com/issues/118475573 and https://chris.banes.dev/2018/02/18/fragmented-transitions/

    0 讨论(0)
  • 2020-12-30 07:11

    Android material design library contains MaterialContainerTransform class which allows to easily implement container transitions including transitions on recycler-view items. See container transform section for more details.

    Here's an example of such a transition:

    // FooListFragment.kt
    
    class FooListFragment : Fragment() {
        ...
    
        private val itemListener = object : FooListener {
            override fun onClick(item: Foo, itemView: View) {
                ...
    
                val transitionName = getString(R.string.foo_details_transition_name)
                val extras = FragmentNavigatorExtras(itemView to transitionName)
                navController.navigate(directions, extras)
            }
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
    
            // Postpone enter transitions to allow shared element transitions to run.
            // https://github.com/googlesamples/android-architecture-components/issues/495
            postponeEnterTransition()
            view.doOnPreDraw { startPostponedEnterTransition() }
    
            ...
        }
    
    // FooDetailsFragment.kt
    
    class FooDetailsFragment : Fragment() {
        ...
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            sharedElementEnterTransition = MaterialContainerTransform().apply {
                duration = 1000
            }
        }
    }
    

    And don't forget to add unique transition names to the views:

    <!-- foo_list_item.xml -->
    
    <LinearLayout ...
        android:transitionName="@{@string/foo_item_transition_name(foo.id)}">...</LinearLayout>
    
    <!-- fragment_foo_details.xml -->
    
    <LinearLayout ...
        android:transitionName="@string/foo_details_transition_name">...</LinearLayout>
    
    <!-- strings.xml -->
    <resources>
        ...
        <string name="foo_item_transition_name" translatable="false">foo_item_transition_%1$s</string>
        <string name="foo_details_transition_name" translatable="false">foo_details_transition</string>
    </resources>
    

    The full sample is available on GitHub.

    You can also take a look at Reply - an official android material sample app where a similar transition is implemented, see HomeFragment.kt & EmailFragment.kt. There's a codelab describing the process of implementing transitions in the app, and a video tutorial.

    0 讨论(0)
  • 2020-12-30 07:14

    Here is my example with RecyclerView that have fragment shared transition. In my adapter i am setting different transition name for each item based on position(In my example it is ImageView).

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = items[position]
        holder.itemView.txtView.text=item
        ViewCompat.setTransitionName(holder.itemView.imgViewIcon, "Test_$position")
        holder.setClickListener(object : ViewHolder.ClickListener {
            override fun onClick(v: View, position: Int) {
                when (v.id) {
                    R.id.linearLayout -> listener.onClick(item, holder.itemView.imgViewIcon, position)
                }
            }
        })
    
    }
    

    And when clicking on item, my interface that implemented in source fragment:

    override fun onClick(text: String, img: ImageView, position: Int) {
        val action = MainFragmentDirections.actionMainFragmentToSecondFragment(text, position)
        val extras = FragmentNavigator.Extras.Builder()
                .addSharedElement(img, ViewCompat.getTransitionName(img)!!)
                .build()
        NavHostFragment.findNavController(this@MainFragment).navigate(action, extras)
    }
    

    And in my destination fragment:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        info("onCreate")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
        }
    }
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        info("onCreateView")
        return inflater.inflate(R.layout.fragment_second, container, false)
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        info("onViewCreated")
        val name=SecondFragmentArgs.fromBundle(arguments).name
        val position=SecondFragmentArgs.fromBundle(arguments).position
        txtViewName.text=name
        ViewCompat.setTransitionName(imgViewSecond, "Test_$position")
    }
    
    0 讨论(0)
  • 2020-12-30 07:23

    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
                        }
    }
    
    0 讨论(0)
提交回复
热议问题