简体   繁体   English

如何使用 Android 导航组件实现从 RecyclerView 项目到 Fragment 的共享过渡元素?

[英]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 .我想在recyclerViewfragment的项目之间实现共享元素转换。 I'm using android navigation component in my app.我在我的应用程序中使用了 android 导航组件。

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 . developer.android上有一篇关于共享转换的文章和stackoverflow上的主题,但此解决方案仅适用于位于开始转换的fragment布局中的视图,不适用于RecyclerView项目。 Also there is a lib on github but i don't want to rely on 3rd party libs and do it by myself. github上也有一个库,但我不想依赖 3rd 方库并自己完成。

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 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在 onItemClick 我通过 recyclerView 中的 textView 表单项进行转换

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.有趣的 transitionWithTextViewInFragment() - 有过渡。

fun transitionWithTextViewInRecyclerViewItem(view: View) - no transition.有趣的 transitionWithTextViewInRecyclerViewItem(view: View) - 没有过渡。

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
                    }
}

Here is my example with RecyclerView that have fragment shared transition.这是我的 RecyclerView 示例,它具有片段共享转换。 In my adapter i am setting different transition name for each item based on position(In my example it is ImageView).在我的适配器中,我根据位置为每个项目设置不同的转换名称(在我的示例中是 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")
}

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 上的许多问题相同的问题,但对我而言,问题的根本原因是Navigation目前仅对片段事务使用replace ,并且每次回击时都会导致开始片段中的回收器重新加载,这是一个问题本身。

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只是在onCreateView添加一个简单的检查

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.所以这里是三赢:回收器没有重绘,没有从服务器重新获取,返回转换也按预期工作。

I have managed return transitions to work.我已经管理返回工作。

Actually this is not a bug in Android and not a problem with setReorderingAllowed = true .实际上,这不是 Android 中的错误,也不是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() .为了解决这个问题,我们必须使用postponeEnterTransition() startPostponedEnterTransition()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/有关更多详细信息和更深入的解释,请参阅: https : //issuetracker.google.com/issues/118475573https://chris.banes.dev/2018/02/18/fragmented-transitions/

Android material design library contains MaterialContainerTransform class which allows to easily implement container transitions including transitions on recycler-view items. Android 材料设计库包含MaterialContainerTransform类,它允许轻松实现容器转换,包括在回收器视图项目上的转换。 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 .完整示例可在 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 .您还可以查看Reply - 一个官方 android 材料示例应用程序,其中实现了类似的转换,请参阅HomeFragment.ktEmailFragment.kt There's a codelab describing the process of implementing transitions in the app, and a video tutorial .有一个代码实验室描述了在应用程序中实现转换的过程,还有一个视频教程

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 Jetpack Navigation 中从 RecyclerView 到 Detail Fragment 的共享元素转换 - Shared element transition in Jetpack Navigation from RecyclerView to Detail Fragment Android:RecyclerView和Fragment之间没有共享元素转换 - Android: No shared element transition between RecyclerView and Fragment 如何在RecyclerView中将共享元素转换为onClickListener? - How to implement shared element transition to onClickListener in RecyclerView? 如何使用从 RecyclerView 到 Fragment 的共享转换 - How to used a shared transition from RecyclerView to a Fragment 共享元素从viewpager内的fragment recyclerview过渡到新活动? - Shared element transition from fragment recyclerview inside viewpager to new activity? 如何共享元素从片段到活动的转换 - how to Shared element transition from a fragment to an activity 使用共享元素的Android片段转换 - Android fragment transition with shared element 如何使用导航组件收听 Android 片段转换完成的转换? - How to Listen to Transition Completed on Android Fragment Transition with Navigation Component? 如何在使用导航组件和 safeArgs 从回收器视图导航到片段时添加共享元素转换? - How to add shared element transition when navigating from recycler view to a fragment while using navigation components and safeArgs? 共享元素从viewPager中的recyclerView转换为fragment - Shared elements transition from recyclerView in viewPager to fragment
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM