嵌套片段转换不正确

Nested fragments transitioning incorrectly

你好堆栈溢出的程序员!我在这个问题上度过了愉快的一周,现在非常绝望 寻求解决方案。


场景

我正在使用 android.app.Fragment,以免与支持片段混淆。

我有 6 个 child 片段命名为:

我有 2 个 parent 片段命名为:

我有 1 个 activity 命名为:

他们的行为如下:

你可能已经猜到了,

FragmentNumeric 显示 child 个片段 FragmentOneFragmentTwoFragmentThree

FragmentAlpha 显示 child 个片段 FragmentAFragmentBFragmentC


问题

我正在尝试 transition/animate parent 和 child 片段。 child 片段过渡顺利且符合预期。然而,当我转换到一个新的 parent 片段时,它看起来很糟糕。 child 片段看起来像是从其 parent 片段运行独立的转换。 child 片段看起来也从 parent 片段中删除了。可以在此处查看它的 gif https://imgur.com/kOAotvk。请注意当我单击 Show Alpha 时会发生什么。

我能找到的最接近的问题和答案在这里:Nested fragments disappear during transition animation 然而所有的答案都是令人不满意的技巧。


动画师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

需要考虑的事项:第一个 parent 片段 FragmentNumeric 没有进入效果,因此它始终与 activity 一起准备就绪,并且没有退出效果,因为没有退出。我也在使用 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()
        }
    }
}

片段数字

在快速显示其第一个 child 片段方面与 activity 做同样的事情。

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 替换片段一、二和三。

Children 片段仅显示以下 XML 视图并动态设置其文本和按钮单击侦听器以从 parent 片段或 activity.

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>

使用 dagger 和一些合约,我有 child 片段回调到他们的 parent 片段和托管活动,方法如下:

FragmentOne 设置按钮点击侦听器:

(parentFragment as FragmentNumeric).showFragmentTwo()

FragmentTwo 设置按钮点击侦听器:

(parentFragment as FragmentNumeric).showFragmentThree()

FragmentThree 不同,它会设置点击监听器来做:

(activity as MainActivity).showFragmentAlpha()

有人能解决这个问题吗?


更新 1

我已按要求添加了示例项目:https://github.com/zafrani/NestedFragmentTransitions

它与我原始视频中的区别是 parent 片段不再使用 xFraction 属性 的视图。所以看起来进入动画不再有重叠效果了。但是,它仍然确实从 parent 中删除了 child 片段并将它们并排设置为动画。动画结束后,片段三立即被片段A替换。

更新 2

parent 和 child 片段视图都使用 xFraction 属性。关键是在 parent 动画时抑制 childs 动画。

你得到的结果如此有线,因为你正在为你的片段使用退出动画。基本上,我们每次都遇到 Fragment 动画这样的问题,最终从源 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) 另一种选择,可能适合,它考虑应用程序结构并尽量避免用动画替换 Fragment。如果你需要替换,不要使用动画,你可以添加任何动画(包括进入和退出),但只能添加。

我想我找到了使用 Fragment#onCreateAnimator 解决此问题的方法。可以在此处查看过渡的 gif:https://imgur.com/94AvrW4.

我制作了一个 PR 用于测试,到目前为止它按我预期的方式工作并且在配置更改后仍然存在并支持后退按钮。这里是linkhttps://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)
    }

如果有人找到更好的方法,将保留问题。