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

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


Сценарий

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

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

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

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

  • FragmentNumeric
  • FragmentAlpha

У меня есть 1 действие с именем:

  • MainActivity

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

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

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

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

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


Проблема

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

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


XML файлы Animator

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

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, заменяя фрагменты One, Two и Three с фрагментами 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. Таким образом, похоже, что анимация ввода не имеет такого перекрывающегося эффекта. Тем не менее он все же удаляет дочерний фрагмент из родителя и анимирует его рядом. После завершения анимации фрагмент 3 немедленно заменяет фрагмент A.

Обновление 2

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

Ответ 1

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

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

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

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

Ответ 2

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

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

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

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