Вложенные фрагменты переходят неправильно

Здравствуйте, хорошие программисты переполнения стека! Я провел хорошую неделю с этой проблемой и теперь очень отчаянно нуждаюсь в решении.


Сценарий

Я использую android.app. Фрагмент не следует путать с фрагментами поддержки.

У меня есть 6 дочерних фрагментов с именем:

  • FragmentOne
  • FragmentTwo
  • FragmentThree
  • FragmentA
  • FragmentB
  • FragmentC

У меня есть 2 родительских фрагмента с именем:

  • FragmentNumeric
  • FragmentAlpha

У меня есть 1 деятельность с именем:

  • MainActivity

Они ведут себя следующим образом:

  • Дочерние фрагменты - это фрагменты, которые показывают только представление, они не показывают и не содержат фрагментов.
  • Родительские фрагменты заполняют весь вид одним дочерним фрагментом. Они могут заменить дочерний фрагмент другими дочерними фрагментами.
  • Активность заполняет большую часть своего представления родительским фрагментом. Он может заменить его другими родительскими фрагментами. Таким образом, в любой момент на экране отображается только один дочерний фрагмент.

Как вы, наверное, догадались,

FragmentNumeric показывает дочерние фрагменты FragmentOne, FragmentTwo, а также FragmentThree,

FragmentAlpha показывает дочерние фрагменты FragmentA, FragmentB, а также FragmentC,


Эта проблема

Я пытаюсь перевести / оживить родительский и дочерний фрагменты. Дочерние фрагменты переходят плавно и, как и ожидалось. Однако когда я перехожу на новый родительский фрагмент, это выглядит ужасно. Дочерний фрагмент выглядит так, как будто он выполняет независимый переход от своего родительского фрагмента. И дочерний фрагмент выглядит так, как будто он также удален из родительского фрагмента. GIF его можно посмотреть здесь https://imgur.com/kOAotvk. Обратите внимание, что происходит, когда я нажимаю Показать альфа.

Ближайший вопрос и ответы, которые я смог найти, находятся здесь: вложенные фрагменты исчезают во время анимации перехода, однако все ответы являются неудовлетворительными взломами.


XML-файлы аниматора

У меня есть следующие эффекты аниматора (длительность тестирования очень велика):

fragment_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="1.0"
        android:valueTo="0" />
</set>

fragment_exit.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="0"
        android:valueTo="-1.0" />
</set>

fragment_pop.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="0"
        android:valueTo="1.0" />
</set>

fragment_push.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000"
        android:interpolator="@android:anim/linear_interpolator"
        android:propertyName="xFraction"
        android:valueFrom="-1.0"
        android:valueTo="0" />
</set>

fragment_nothing.xml

<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="4000" />
</set>

MainActivity.kt

Что нужно учесть: Первый родительский фрагмент, FragmentNumeric, не имеет эффектов ввода, поэтому он всегда готов к действию и не имеет эффектов выхода, потому что ничего не происходит. Я также использую FragmentTransaction#add с этим, где, как FragmentAlpha использует FragmentTransaction#replace

class MainActivity : AppCompatActivity {

    fun showFragmentNumeric(){
        this.fragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_nothing,
                                     R.animator.fragment_nothing,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .add(this.contentId, FragmentNumeric(), "FragmentNumeric")
                .addToBackStack("FragmentNumeric")
                .commit()
}

    fun showFragmentAlpha(){
        this.fragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_enter,
                                     R.animator.fragment_exit,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .replace(this.contentId, FragmentAlpha(), "FragmentAlpha")
                .addToBackStack("FragmentAlpha")
                .commit()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            showFragmentNumeric()
        }
    }
}

FragmentNumeric

Делает то же самое, что и действие, в плане быстрого показа своего первого дочернего фрагмента.

class FragmentNumeric : Fragment {

    fun showFragmentOne(){
            this.childFragmentManager.beginTransaction()
                    .setCustomAnimations(R.animator.fragment_nothing,
                                         R.animator.fragment_nothing,
                                         R.animator.fragment_push,
                                         R.animator.fragment_pop)
                    .add(this.contentId, FragmentOne(), "FragmentOne")
                    .addToBackStack("FragmentOne")
                    .commit()
    }

    fun showFragmentTwo(){
        this.childFragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_enter,
                                     R.animator.fragment_exit,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .replace(this.contentId, FragmentTwo(), "FragmentTwo")
                .addToBackStack("FragmentTwo")
                .commit()
    }


    fun showFragmentThree(){
        this.childFragmentManager.beginTransaction()
                .setCustomAnimations(R.animator.fragment_enter,
                                     R.animator.fragment_exit,
                                     R.animator.fragment_push,
                                     R.animator.fragment_pop)
                .replace(this.contentId, FragmentThree(), "FragmentThree")
                .addToBackStack("FragmentThree")
                .commit()
    }

    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        if (savedInstanceState == null) {
            if (this.childFragmentManager.backStackEntryCount <= 1) {
                showFragmentOne()
            }
        }
    }
}

Другие фрагменты

FragmentAlpha следует тому же шаблону, что и FragmentNumeric, заменяя фрагменты один, два и три фрагментами A, B и C соответственно.

Дочерние фрагменты просто показывают следующее представление XML и динамически устанавливают его прослушиватель текста и нажатия кнопки, чтобы вызвать функцию из родительского фрагмента или действия.

view_child_example.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/background"
    android:clickable="true"
    android:focusable="true"
    android:orientation="vertical">

    <TextView
        android:id="@+id/view_child_example_header"
        style="@style/Header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />


    <Button
        android:id="@+id/view_child_example_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"  />
</LinearLayout>

Используя кинжал и некоторые контракты, я выполняю функцию обратного вызова дочерних фрагментов к родительским фрагментам и действия хостинга, выполняя что-то вроде ниже:

FragmentOne устанавливает прослушиватель нажатия кнопки для:

(parentFragment as FragmentNumeric).showFragmentTwo()

FragmentTwo устанавливает прослушиватель нажатия кнопки для:

(parentFragment as FragmentNumeric).showFragmentThree()

FragmentThree отличается, он установит прослушиватель кликов следующим образом:

(activity as MainActivity).showFragmentAlpha()

У кого-нибудь есть решение этой проблемы?


Обновление 1

Я добавил пример проекта по запросу: https://github.com/zafrani/NestedFragmentTransitions

Разница в этом и в том, что в моем исходном видео родительский фрагмент больше не использует представление со свойством xFraction. Похоже, анимация ввода больше не имеет такого перекрывающегося эффекта. Однако он по-прежнему удаляет дочерний фрагмент из родительского и анимирует их рядом. После завершения анимации третий фрагмент мгновенно заменяется фрагментом А.

Обновление 2

И родительский, и дочерний представления фрагментов используют свойство xFraction. Ключ должен подавить анимацию потомков, когда родительский анимация.

2 ответа

Решение

Я думаю, что нашел способ решить эту проблему, используя Fragment#onCreateAnimator. GIF перехода можно посмотреть здесь: https://imgur.com/94AvrW4.

Я сделал PR для тестирования, пока он работает, как я ожидаю, и пережил изменения конфигурации и поддерживает кнопку возврата. Вот ссылка https://github.com/zafrani/NestedFragmentTransitions/pull/1/files

BaseFragment для родительского и дочернего фрагментов делает это для onCreateAnimator()

override fun onCreateAnimator(transit: Int, enter: Boolean, nextAnim: Int): Animator {
    if (isConfigChange) {
        resetStates()
        return nothingAnim()
    }

    if (parentFragment is ParentFragment) {
        if ((parentFragment as BaseFragment).isPopping) {
            return nothingAnim()
        }
    }

    if (parentFragment != null && parentFragment.isRemoving) {
        return nothingAnim()
    }

    if (enter) {
        if (isPopping) {
            resetStates()
            return pushAnim()
        }
        if (isSuppressing) {
            resetStates()
            return nothingAnim()
        }
        return enterAnim()
    }

    if (isPopping) {
        resetStates()
        return popAnim()
    }

    if (isSuppressing) {
        resetStates()
        return nothingAnim()
    }

    return exitAnim()
}

Булевы значения устанавливаются в разных сценариях, которые легче увидеть в PR.

Функции анимации:

private fun enterAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_enter)
    }

    private fun exitAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_exit)
    }

    private fun pushAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_push)
    }

    private fun popAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_pop)
    }

    private fun nothingAnim(): Animator { 
        return AnimatorInflater.loadAnimator(activity, R.animator.fragment_nothing)
    }

Оставьте вопрос открытым, если кто-то найдет лучший способ.

Вы получаете такой результат, потому что вы используете анимацию выхода для своего фрагмента. По сути, у нас возникают такие проблемы с анимацией фрагмента каждый раз, и, наконец, мы переходим от исходной анимации фрагмента к использованию собственной, каждый раз, когда мы выполняем переход.

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

getFragmentManager().beginTransaction()
                .setCustomAnimations(R.animator.enter_anim_frag1,
                                     0,
                                     R.animator.enter_anim_frag2,
                                     0)
                .replace(xxx, Xxx1, Xxx2)
                .addToBackStack(null)
                .commit()

2) Другой вариант, который может подойти, это подумать о структуре приложения и попытаться избежать замены фрагмента анимацией. Если вам нужно заменить, не используйте анимацию, и вы можете добавить любую анимацию (включая как вход, так и выход), но только с добавлением.