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

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


Сценарий

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

У меня есть 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()
        }
    }
}

ФрагментЧисловой

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

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. Ключ в том, чтобы подавить дочернюю анимацию, когда родитель анимирует.


comment
Вопрос уже предельно ясен, но поскольку у вас, кажется, есть чистый проект, показывающий проблему, было бы здорово, если бы вы могли загрузить его куда-нибудь.   -  person natario    schedule 23.10.2017
comment
Пожалуйста, обновите Gifs, кажется, их нет. УПД. Извините, мой плохой, нашел изображение.   -  person GensaGames    schedule 23.10.2017
comment
@natario Я добавил ссылку на github.   -  person zafrani    schedule 23.10.2017


Ответы (2)


Я думаю, что нашел способ решить эту проблему, используя Fragment#onCreateAnimator. GIF перехода можно посмотреть здесь: 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)
    }

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

person zafrani    schedule 23.10.2017

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

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

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

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

person GensaGames    schedule 23.10.2017