【问题标题】:How to implement shared transition element from RecyclerView item to Fragment with Android Navigation Component?如何使用 Android 导航组件实现从 RecyclerView 项目到 Fragment 的共享过渡元素?
【发布时间】:2018-12-04 13:50:28
【问题描述】:

我有一个非常简单的案例。我想在recyclerViewfragment 中的项目之间实现共享元素转换。我在我的应用程序中使用 android 导航组件。

有一篇关于developer.android 上的共享转换和stackoverflow 上的主题的文章,但此解决方案仅适用于位于fragment 布局中的视图,该布局开始转换并且不适用于来自RecyclerView 的项目。 github 上也有一个库,但我不想依赖第 3 方库并自己做。

有解决办法吗?也许它应该工作,这只是一个错误?但是我没有找到任何关于它的信息。

代码示例:

过渡开始

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

}

布局

<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" />

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

在 onItemClick 中我通过 recyclerView 中的 textView 表单项进行转换

过渡结束

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)

    }
}

布局

<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() - 有过渡。

fun transitionWithTextViewInRecyclerViewItem(view: View) - 没有过渡。

【问题讨论】:

  • 你需要为每个recyclerview item textview设置不同的transition name,并在destination fragment中设置这个transition name。

标签: android android-recyclerview shared-element-transition android-architecture-navigation


【解决方案1】:

要解决返回转换问题,您需要在初始化回收器视图的源片段(具有回收器视图的片段)上添加此行

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

【讨论】:

  • 谢谢,我会检查一下。
  • 当我按下详细片段时它不知何故无法正常工作,我的情况是列表页面位于 viewpager 内,它也托管在片段内,github.com/muhrahmatullah/televisi/blob/master/app/src/main/…
  • ohhh,终于让它工作了,因为我使用了viewpager,所以我应该调用父片段而不是直接调用推迟EnterTransition,所以代码应该是这样的: parentFragment?.postponeEnterTransition() ...
  • 非常好的兄弟。浪费了两个小时来弄清楚退出过渡。
【解决方案2】:

这是我的 RecyclerView 示例,它具有片段共享转换。 在我的适配器中,我根据位置为每个项目设置不同的过渡名称(在我的示例中是 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)
            }
        }
    })

}

当点击项目时,我在源代码片段中实现的界面:

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

在我的目标片段中:

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

【讨论】:

  • 谢谢,它有效。但是现在我遇到了另一个问题:) 有进入转换但在返回转换时。
  • @AlexandrSushkov 在 FragmentTransaction 上使用 setReorderingAllowed=true 时,返回共享转换到 RecyclerView 存在问题,导航组件中始终设置此标志的问题(您可以查看“导航”方法的源代码)。这有一个未解决的问题:issuetracker.google.com/issues/118475573
  • 再次感谢您。
  • @AlexandrSushkov 您是否设法使返回转换工作?
  • @maxbeaudoin no
【解决方案3】:

在 SO 上面临与返回转换相同的问题,但对我而言,问题的根本原因是 Navigation 目前仅将 replace 用于片段事务,它导致我在开始片段中的回收器重新加载每个你反击的时间本身就是一个问题。

因此,通过解决第二个(根本)问题,返回过渡开始工作而没有延迟动画。对于那些希望在回击时保持初始状态的人,我就是这样做的:

只需在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
            }
        }

所以这里是三赢:recycler 没有重绘,没有从服务器重新获取,并且返回转换按预期工作。

【讨论】:

  • 在没有数据绑定的情况下仍然可以做到吗?
  • 保存你的视图是一个变量并返回它而不是绑定实例@KitMak
  • 绑定应该在视图被销毁时设置为空,否则这是一个泄漏!
  • @SamuelEminet 如果您的片段没有被销毁并保留在内存中,您也可以保留视图,否则在返回堆栈时无法保留视图状态。
【解决方案4】:

我已经管理了返回工作的过渡。

实际上这不是 Android 的错误,也不是 setReorderingAllowed = true 的问题。这里发生的是(我们返回的)原始片段试图在其视图/数据确定之前开始转换。

要解决这个问题,我们必须使用 postponeEnterTransition()startPostponedEnterTransition()

例如: 原始片段:

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

下一个片段:

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

}

更多细节和更深入的解释见: https://issuetracker.google.com/issues/118475573https://chris.banes.dev/2018/02/18/fragmented-transitions/

【讨论】:

    【解决方案5】:

    Android material design library 包含MaterialContainerTransform 类,它允许轻松实现容器转换,包括对回收器视图项的转换。有关详细信息,请参阅container transform 部分。

    以下是此类转换的示例:

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

    别忘了给视图添加唯一的过渡名称:

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

    完整样本为available on GitHub

    您还可以查看Reply - 一个实现了类似过渡的官方 android 材质示例应用程序,请参阅HomeFragment.ktEmailFragment.kt。有一个 codelab 描述了在应用程序中实现转换的过程,还有一个 video tutorial

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-20
      • 2016-02-04
      相关资源
      最近更新 更多