Как реализовать общий элемент перехода от элемента RcyclerView к фрагменту с помощью компонента навигации Android?

У меня довольно простой случай. Я хочу реализовать общий переход элемента между элементом в RececlerView а также fragment, Я использую компонент навигации Android в моем приложении.

Есть статья о совместном переходе на developer.android и тема о stackru, но это решение работает только для просмотра, расположенного в fragment макет, который начинает переход и не работает для элементов из RecyclerView, Также на github есть библиотека, но я не хочу полагаться на сторонние библиотеки и делаю это сам.

Есть ли какое-то решение для этого? Может быть, это должно работать, и это просто ошибка? Но я не нашел никакой информации об этом.

Пример кода:

начало перехода

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

адаптер для реселлераПросмотреть

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() - имеет переход.

забавный переход WithTextViewInRecyclerViewItem(представление: представление) - нет перехода.

1 ответ

Решение

Вот мой пример с 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")
}

Чтобы решить проблему с обратным переходом, вам нужно добавить эти строки во фрагмент исходного кода (фрагмент с представлением переработчика), где вы инициализируете представление переработчика.

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

Столкнулся с той же проблемой, что и многие на 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
            }
        }

Итак, тройная победа: ресайклер не перерисовывается, никакая повторная выборка с сервера, а также возвратные переходы работают должным образом.

Библиотека дизайна материалов Android содержит класс MaterialContainerTransform, который позволяет легко реализовывать переходы контейнеров, в том числе переходы на элементах представления ресайклера. См. Раздел о преобразовании контейнера для более подробной информации.

Вот пример такого перехода:

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

Полный пример доступен на GitHub. Вы также можете взглянуть на Reply - официальный образец материала для Android, в котором реализован аналогичный переход, см. HomeFragment.kt & EmailFragment.kt.

Мне удалось вернуться к работе.

На самом деле это не ошибка 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/118475573 и https://chris.banes.dev/2018/02/18/fragmented-transitions/