如何使用 Android 导航组件实现从 RecyclerView 项目到 Fragment 的共享过渡元素?
How to implement shared transition element from RecyclerView item to Fragment with Android Navigation Component?
我有一个非常简单的案例。我想在 recyclerView
和 fragment
中的项目之间实现共享元素转换。我在我的应用程序中使用 android 导航组件。
developer.android and topic on 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 上有一篇关于共享转换的文章,但我不想依赖第 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 中我将 textView 表单项传递到 recyclerView 中进行转换
过渡结束
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>
有趣的 transitionWithTextViewInFragment() - 有转换。
有趣的 transitionWithTextViewInRecyclerViewItem(view: View) - 没有过渡。
这是我使用具有片段共享过渡的 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")
}
要解决 return 转换问题,您需要在初始化回收器视图的源片段(具有回收器视图的片段)上添加此行
// your recyclerView
recyclerView.apply {
...
adapter = myAdapter
postponeEnterTransition()
viewTreeObserver
.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
在 return 转换过程中遇到了与 SO 上许多人相同的问题,但对我来说,问题的根本原因是 Navigation
目前仅使用 replace
进行片段事务,它导致我在开始片段中的回收器在每次你回击时重新加载,这本身就是一个问题。
因此,通过解决第二个(根本)问题,return 过渡开始工作,没有延迟动画。对于那些希望在返回时保持初始状态的人,我就是这样做的:
只需在 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
}
}
所以三赢在这里:回收器没有重绘,没有从服务器重新获取,而且 return 转换按预期工作。
我已经 return 过渡到工作。
实际上这不是 Android 中的错误,也不是 setReorderingAllowed = true
中的问题。这里发生的是原始片段(我们 return)试图在 其 views/data 完成之前开始转换 。
要解决这个问题,我们必须使用 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/118475573
和
https://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. See 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 - 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。
我有一个非常简单的案例。我想在 recyclerView
和 fragment
中的项目之间实现共享元素转换。我在我的应用程序中使用 android 导航组件。
developer.android and topic on fragment
layout that starts transition and doesn't work for items from RecyclerView
. Also there is a lib on 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 中我将 textView 表单项传递到 recyclerView 中进行转换
过渡结束
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>
有趣的 transitionWithTextViewInFragment() - 有转换。
有趣的 transitionWithTextViewInRecyclerViewItem(view: View) - 没有过渡。
这是我使用具有片段共享过渡的 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")
}
要解决 return 转换问题,您需要在初始化回收器视图的源片段(具有回收器视图的片段)上添加此行
// your recyclerView
recyclerView.apply {
...
adapter = myAdapter
postponeEnterTransition()
viewTreeObserver
.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
在 return 转换过程中遇到了与 SO 上许多人相同的问题,但对我来说,问题的根本原因是 Navigation
目前仅使用 replace
进行片段事务,它导致我在开始片段中的回收器在每次你回击时重新加载,这本身就是一个问题。
因此,通过解决第二个(根本)问题,return 过渡开始工作,没有延迟动画。对于那些希望在返回时保持初始状态的人,我就是这样做的:
只需在 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
}
}
所以三赢在这里:回收器没有重绘,没有从服务器重新获取,而且 return 转换按预期工作。
我已经 return 过渡到工作。
实际上这不是 Android 中的错误,也不是 setReorderingAllowed = true
中的问题。这里发生的是原始片段(我们 return)试图在 其 views/data 完成之前开始转换 。
要解决这个问题,我们必须使用 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/118475573 和 https://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. See 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 - 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。