嵌套片段转换不正确

18

大家好,我是来自 Stack Overflow 的优秀程序员!我已经花了一个星期的时间来解决这个问题,现在非常绝望,希望能得到解决。


场景

我正在使用 android.app.Fragment,不要与 support fragments 混淆。

我有6个子片段,分别为:

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

我有2个父片段,分别为:

  • FragmentNumeric
  • FragmentAlpha

它们的行为如下:

  • 子片段仅显示视图,不显示也不包含其他片段。
  • 父片段填充其整个视图并显示单个子片段。 它们可以用其他子片段替换子片段。
  • 活动将其大部分视图填充为父片段。 它可以用其他父片段替换它。 因此,在任何一时刻,屏幕上只显示一个子片段。

正如您可能猜到的那样,

FragmentNumeric 显示子片段 FragmentOneFragmentTwoFragmentThree

FragmentAlpha 显示子片段 FragmentAFragmentBFragmentC


问题

我正在尝试过渡/动画父片段和子片段。 子片段过渡平滑,效果如预期。 但是当我转换到新的父片段时,它看起来很糟糕。 子片段看起来像从其父片段独立运行过渡一样。 子片段还似乎已从其父片段中移除。 可以在此处查看其 GIF 动画:https://imgur.com/kOAotvk。 注意单击“Show Alpha”时发生的情况。

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


Animator XML Files

我有以下动画效果(为了测试目的,持续时间很长):

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相同的模式,将Fragments One、Two和Three分别替换为Fragments 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>

使用 Dagger 和一些协议,我让子片段通过以下方式回调到其父片段和宿主活动:

FragmentOne 设置按钮的点击侦听器执行以下操作:

(parentFragment as FragmentNumeric).showFragmentTwo()

FragmentTwo设置按钮点击监听器以执行:

(parentFragment as FragmentNumeric).showFragmentThree()

FragmentThree 不同,它将设置点击监听器以执行:

(activity as MainActivity).showFragmentAlpha()

有人有解决这个问题的方法吗?


更新1

如请求所示,我添加了一个示例项目:https://github.com/zafrani/NestedFragmentTransitions

与原始视频中不同的是,父片段不再使用具有xFraction属性的视图。因此,进入动画不再具有重叠效果。但它仍然会将子片段从父级中移除并将它们并排动画。动画完成后,Fragment Three 立即被 Fragment A替换。

更新2

父片段和子片段视图都使用 xFraction 属性。关键在于在父项动画时抑制子项的动画。


3
问题已经非常清楚了,但由于您似乎有一个展示问题的干净项目,如果您能将其上传到某个地方就更好了。 - natario
{btsdaf} - GensaGames
{btsdaf} - zafrani
2个回答

4

我认为我已经找到了使用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)
    }

留下这个问题,以防有人找到更好的方法。


0

你得到了如此奇怪的结果,是因为你正在为你的Fragment使用退出动画。基本上,我们每次都会遇到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) 另外一个选择,可能更适合的是考虑应用程序结构并尝试避免使用动画替换片段。如果您需要进行替换,请不要使用动画,您只能通过添加来添加任何动画(包括进入和退出)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接