嵌套片段过渡不正确

Posted

技术标签:

【中文标题】嵌套片段过渡不正确【英文标题】:Nested fragments transitioning incorrectly 【发布时间】:2018-04-02 05:13:01 【问题描述】:

你好堆栈溢出的优秀程序员!我已经为这个问题度过了愉快的一周,现在非常迫切需要解决方案。


场景

我使用的是 android.app.Fragment,不要与支持 Fragment 混淆。

我有 6 个子片段,名为:

FragmentOne FragmentTwo FragmentThree FragmentA FragmentB FragmentC

我有 2 个名为的父片段:

FragmentNumeric FragmentAlpha

我有 1 个活动名为:

MainActivity

它们的行为如下:

子片段是只显示视图的片段,它们不显示也不包含片段。 父片段用一个子片段填充其整个视图。他们能够用其他子片段替换子片段。 活动用父片段填充其大部分视图。它可以用其他父片段替换它。 因此,在任何时候,屏幕都只会显示一个子片段。

你可能已经猜到了,

FragmentNumeric 显示子片段 FragmentOneFragmentTwoFragmentThree

FragmentAlpha 显示子片段 FragmentAFragmentBFragmentC


问题

我正在尝试对父片段和子片段进行过渡/动画处理。子片段按预期平稳过渡。但是,当我过渡到新的父片段时,它看起来很糟糕。子片段看起来像是从其父片段运行独立的过渡。并且子片段看起来也从父片段中删除了。可以在这里查看它的 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

需要考虑的事项:第一个父片段 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()
        
    


片段数字

在快速显示其第一个子片段方面与 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 替换片段一、二和三。

子片段只是显示以下 XML 视图并动态设置其文本和按钮单击侦听器以从父片段或活动调用函数。

view_child_example.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_
    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_
        android:layout_ />


    <Button
        android:id="@+id/view_child_example_button"
        android:layout_
        android:layout_  />
</LinearLayout>

使用匕首和一些合同,我让子片段回调到它们的父片段并通过执行以下操作来托管活动:

FragmentOne 设置按钮点击监听器来做:

(parentFragment as FragmentNumeric).showFragmentTwo()

FragmentTwo 设置按钮点击监听器要做的事情:

(parentFragment as FragmentNumeric).showFragmentThree()

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

(activity as MainActivity).showFragmentAlpha()

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


更新 1

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

它与我原始视频中的不同之处在于父片段不再使用具有 xFraction 属性的视图。所以看起来进入动画不再有那种重叠的效果了。然而,它仍然会从父片段中移除子片段并并排设置动画。动画完成后,片段三立即被片段A替换。

更新 2

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

【问题讨论】:

问题已经非常清楚了,但是由于您似乎有一个干净的项目来显示该问题,因此如果您可以将其上传到某个地方会很酷。 请更新 Gif,好像没了。 UPD。对不起,我的错,找到了图片。 @natario 我添加了一个 github 链接。 【参考方案1】:

我想我已经找到了一种使用 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)
    

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

【讨论】:

【参考方案2】:

你得到如此连贯的结果,因为你正在为你的片段使用退出动画。基本上,我们每次使用 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) 另一个可能适合的选项,它考虑应用程序结构并尽量避免用动画替换片段。如果需要替换,不要使用动画,可以添加任何动画(包括进入和退出),但只能添加。

【讨论】:

以上是关于嵌套片段过渡不正确的主要内容,如果未能解决你的问题,请参考以下文章

java 将循环显示和不显示过渡动画添加到Android片段

共享元素返回过渡不适用于片段中的 recyclerview 和 cardview

使用片段共享过渡时返回过渡无法正常工作

5秒后Android返回过渡不起作用

FragmentNavigator 共享过渡不起作用

丑陋的片段过渡到带有覆盖的surfaceview