嵌套片段在转场动画期间消失

102

这是情景:Activity 包含 Fragment A,然后在它的 onCreate 中使用 getChildFragmentManager() 添加 Fragment A1A2,如下所示:

getChildFragmentManager()
  .beginTransaction()
  .replace(R.id.fragmentOneHolder, new FragmentA1())
  .replace(R.id.fragmentTwoHolder, new FragmentA2())
  .commit()

目前为止,一切都如预期地运行良好。

接下来,我们在活动中运行以下事务:

getSupportFragmentManager()
  .beginTransaction()
  .setCustomAnimations(anim1, anim2, anim1, anim2)
  .replace(R.id.fragmentHolder, new FragmentB())
  .addToBackStack(null)
  .commit()
在过渡期间,碎片B的进入动画正确运行,但碎片A1和A2完全消失。当我们使用“返回”按钮恢复事务时,它们会正常初始化并在popEnter动画期间正常显示。
在我的简短测试中,情况变得更奇怪了 - 如果我设置子碎片的动画(请参见下文),则在添加碎片B时,退出动画会间歇性地运行。
getChildFragmentManager()
  .beginTransaction()
  .setCustomAnimations(enter, exit)
  .replace(R.id.fragmentOneHolder, new FragmentA1())
  .replace(R.id.fragmentTwoHolder, new FragmentA2())
  .commit()
我想要实现的效果很简单-我希望在片段A(anim2)上运行exit(或者应该是popExit?)动画,包括其嵌套子项在内的整个容器进行动画处理。是否有任何方法可以实现这一点? 编辑:请在此处找到一个测试用例。 编辑2:感谢@StevenByle推动我坚持使用静态动画。显然,您可以根据操作设置动画(不全局设置整个事务),这意味着子元素可以具有无限制的静态动画集,而它们的父项可以具有不同的动画,整个过程可以在一个事务中提交。请参见下面的讨论和更新的测试案例项目

关于A、A1、A2等,R.id.fragmentHolder是什么? - CommonsWare
你在问题中提到的更改(替换起始片段)是你真正想要做的还是只是一个例子?你只会调用changeFragment方法一次吗? - user
为了防止内部片段被销毁(并随着父级的动画而消失),您可以隐藏它,而不是使用简单的“替换”。因此,在changeFragment方法中的事务将有两个操作,一个是隐藏FragmentA,另一个是将FragmentB添加到容器中。这应该适用于退出和进入动画。 - user
你觉得像这个 https://gist.github.com/luksprog/4996190 这样模拟嵌套片段的存在怎么样(请记住,我写的只是非常基础的)? - user
@Luksprog - 简直是太巧妙了,我喜欢!:) 我会试一试的。甚至可能尝试保存绘图缓存,而不是从头开始绘制层次结构。StevenByle - 我以为FragmentTransactions只能持有一个自定义动画集(所以删除子项的动画会继承删除父项的动画,从而增加速度),但是看了实现后,每个操作可以使用不同的动画是完全可能的。我会试一试并告诉你结果。 - Delyan
显示剩余9条评论
16个回答

68

看起来有很多不同的解决方法,但基于@Jayd16的答案,我认为我找到了一个相当稳定的通用解决方案,仍然允许自定义子片段的过渡动画,并且不需要对布局进行位图缓存。

创建一个名为BaseFragment 的类,它扩展了Fragment,并使所有的片段都扩展该类(不仅仅是子片段)。

在这个BaseFragment类中,添加以下内容:

// Arbitrary value; set it to some reasonable default
private static final int DEFAULT_CHILD_ANIMATION_DURATION = 250;

@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
    final Fragment parent = getParentFragment();

    // Apply the workaround only if this is a child fragment, and the parent
    // is being removed.
    if (!enter && parent != null && parent.isRemoving()) {
        // This is a workaround for the bug where child fragments disappear when
        // the parent is removed (as all children are first removed from the parent)
        // See https://code.google.com/p/android/issues/detail?id=55228
        Animation doNothingAnim = new AlphaAnimation(1, 1);
        doNothingAnim.setDuration(getNextAnimationDuration(parent, DEFAULT_CHILD_ANIMATION_DURATION));
        return doNothingAnim;
    } else {
        return super.onCreateAnimation(transit, enter, nextAnim);
    }
}

private static long getNextAnimationDuration(Fragment fragment, long defValue) {
    try {
        // Attempt to get the resource ID of the next animation that
        // will be applied to the given fragment.
        Field nextAnimField = Fragment.class.getDeclaredField("mNextAnim");
        nextAnimField.setAccessible(true);
        int nextAnimResource = nextAnimField.getInt(fragment);
        Animation nextAnim = AnimationUtils.loadAnimation(fragment.getActivity(), nextAnimResource);

        // ...and if it can be loaded, return that animation's duration
        return (nextAnim == null) ? defValue : nextAnim.getDuration();
    } catch (NoSuchFieldException|IllegalAccessException|Resources.NotFoundException ex) {
        Log.w(TAG, "Unable to load next animation from parent.", ex);
        return defValue;
    }
}

不幸的是,这需要使用反射;但是,由于此解决方法适用于支持库,因此除非您更新支持库,否则不会出现底层实现更改的风险。如果您正在从源代码构建支持库,则可以在 Fragment.java 中添加下一个动画资源ID的访问器,从而消除对反射的需求。

这个解决方案消除了“猜测”父级动画持续时间的需要(以便“什么也不做”的动画将与父级退出动画具有相同的持续时间),并允许您仍然对子片段进行自定义动画(例如,如果您正在使用不同的动画交换子片段)。


5
这是我在帖子中最喜欢的解决方案。它不需要位图,也不需要在子片段中添加任何额外的代码,而且实际上也不会从父片段泄露信息给子片段。 - jacobhyphenated
1
@EugenPechanec,你需要从片段获取nextAnim,而不是从子片段获取。这就是整个重点。 - Kevin Coppock
1
不幸的是,在Lollipop以下的Android版本中,这种方法会导致子片段和父片段隐式地发生内存泄漏 :( - Cosmin
8
非常感谢您提供了这个非常有用的解决方案,不过它需要稍作更新才能与当前的支持库(27.0.2,不知道哪个版本破坏了这些代码)配合使用。 mNextAnim 现在位于一个 mAnimationInfo 对象中。您可以按如下方式访问它:Field animInfoField = Fragment.class.getDeclaredField("mAnimationInfo"); animInfoField.setAccessible(true); Object animationInfo = animInfoField.get(fragment); Field nextAnimField = animationInfo.getClass().getDeclaredField("mNextAnim"); - David Lericolais
5
@DavidLericolais想在你的代码后面再加一行代码。将int nextAnimResource = nextAnimField.getInt(fragment);这行代码替换为val nextAnimResource = nextAnimField.getInt(animationInfo);。请注意,翻译旨在使原文更易于理解,但不会改变其含义或添加其他信息。 - tingyik90
显示剩余10条评论

36
为了避免在事务中删除/替换父片段时,用户看到嵌套片段消失,您可以通过提供它们的图像来“模拟”这些片段仍然存在,就像它们出现在屏幕上一样。该图像将用作嵌套片段容器的背景,因此即使嵌套片段的视图消失,图像也会模拟它们的存在。此外,我认为丢失嵌套片段视图的交互性不是问题,因为我认为当它们正在被删除(可能也是用户操作)时,您不希望用户对它们进行操作。
我已经做了一个小例子,设置了背景图像(基础内容)。

1
我决定授予您赏金,因为那是我最终采用的解决方案。非常感谢您的时间! - Delyan
19
哇,好肮脏啊。我们安卓开发者为了追求流畅性不得不付出很大的努力。 - Dean Wild
1
我有一个Viewpager,是从第二个选项卡替换其他片段的。当我在它上面按返回键时,我需要显示Viewpager第二个选项卡,它打开了,但是显示空白页面。我尝试了你在上面的线程中建议的方法,但仍然是同样的情况。 - Harish
当用户回来时,问题仍然存在,正如@Harish所写的那样。 - Ewoks
1
哇,真的吗?现在都2018年了,这还是个问题?:( - Archie G. Quiñones
显示剩余2条评论

32

我成功地想出了一个相当干净的解决方案。在我看来,这是最不破坏性的解决方案,虽然从技术上讲它是“绘制位图”的解决方案,但至少通过片段库进行了抽象化。

确保您的子片段重写了父类的这个方法:

private static final Animation dummyAnimation = new AlphaAnimation(1,1);
static{
    dummyAnimation.setDuration(500);
}

@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
    if(!enter && getParentFragment() != null){
        return dummyAnimation;
    }
    return super.onCreateAnimation(transit, enter, nextAnim);
}

如果我们在子碎片上设置了一个退出动画,它们将被动画化而不是消失。 我们可以利用这一点,使用一个简单的动画,在一段时间内将子碎片以完全不透明度绘制出来。 这样,当父级碎片进行动画时,它们将保持可见,从而实现所需的行为。

唯一的问题是要跟踪这段时间。 我可以把它设定为一个相对较大的数字,但我担心如果仍然在某个地方绘制该动画,可能会有性能问题。


很有帮助,谢谢。持续时间的值并不重要。 - iscariot
到目前为止,最干净的解决方案 - Liran Cohen

16

为了清晰明了,我将发布我的解决方案。解决方案非常简单。如果您正在尝试模仿父片段的事务动画,只需在子片段事务中添加自定义动画并设置相同的持续时间即可。哦,还要确保在add()之前设置自定义动画。

getChildFragmentManager().beginTransaction()
        .setCustomAnimations(R.anim.none, R.anim.none, R.anim.none, R.anim.none)
        .add(R.id.container, nestedFragment)
        .commit();

R.anim.none的XML文件(我父母进入/退出动画时间为250毫秒)

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="0" android:toXDelta="0" android:duration="250" />
</set>

我做了非常相似的事情,但在更新子项时使用了“show”而不是“add”。我还添加了“getChildFragmentManager().executePendingTransactions()”,尽管我不确定是否严格需要。这个解决方案完全可行,而且不需要像一些人建议的那样“提供Fragment的图像”。 - Brian Yencho
这很棒。然而,当我切换我的子片段时,我遇到了延迟。为了避免这种情况,只需将第二个参数设置为无动画:fragmentTransaction.setCustomAnimations(R.anim.none, 0, R.anim.none, R.anim.none) - ono

7
我知道这可能不能完全解决你的问题,但也许它会适合其他人的需求。你可以给那些不实际移动/动画的子 Fragment 添加 enter/exitpopEnter/popExit 动画。只要这些动画与其父 Fragment 的动画持续时间/偏移量相同,它们就会看起来随着父级动画而移动/动画。

1
我已将赏金授予Luksprog,因为他的解决方案在所有情况下都有效。我确实尝试了静态动画技巧(实际持续时间并不重要 - 一旦父级消失,视图显然会消失),但它们并不适用于所有可能的情况(请参见我在问题下的评论)。此外,这种方法泄漏了抽象性,因为具有子项的片段的父项需要知道这个事实并采取额外步骤来设置子项的动画。无论如何,非常感谢您的时间! - Delyan
同意,这更像是一种权宜之计而不是一个完美的解决方案,可以认为有些脆弱。但对于简单的情况来说,它是可行的。 - Steven Byle

4
您可以在子片段中完成此操作。
@Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
    if (true) {//condition
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(getView(), "alpha", 1, 1);
        objectAnimator.setDuration(333);//time same with parent fragment's animation
        return objectAnimator;
    }
    return super.onCreateAnimator(transit, enter, nextAnim);
}

谢谢!也许不是最好的,但可能是最简单的解决方案。 - bug56

2

我遇到了与地图片段相关的相同问题。在包含的片段退出动画期间,它会消失。解决方法是为子地图片段添加动画,这将使其在父片段的退出动画期间保持可见。子片段的动画在其持续时间内保持alpha值为100%。

动画:res/animator/keep_child_fragment.xml

<?xml version="1.0" encoding="utf-8"?>    
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:propertyName="alpha"
        android:valueFrom="1.0"
        android:valueTo="1.0"
        android:duration="@integer/keep_child_fragment_animation_duration" />
</set>

当地图片段被添加到父片段中时,动画效果将被应用。 父片段
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.map_parent_fragment, container, false);

    MapFragment mapFragment =  MapFragment.newInstance();

    getChildFragmentManager().beginTransaction()
            .setCustomAnimations(R.animator.keep_child_fragment, 0, 0, 0)
            .add(R.id.map, mapFragment)
            .commit();

    return view;
}

最后,子片段动画的持续时间在资源文件中设置。 values/integers.xml
<resources>
  <integer name="keep_child_fragment_animation_duration">500</integer>
</resources>

2

编辑: 我最终取消了此解决方案,因为它存在其他问题。Square最近推出了两个替代片段的库。我认为这可能比尝试将片段强制执行不想要的操作更好。

http://corner.squareup.com/2014/01/mortar-and-flow.html

@@@@@@@@@@@@@@@@@@@@@@@@@@@@

我想提供这个解决方案,以帮助未来遇到这个问题的人们。如果您跟踪原始帖子者与其他人的对话,并查看他发布的代码,您会发现原始帖子者最终得出结论:在动画父片段时,在子片段上使用无操作动画。这个解决方案并不理想,因为它强制您跟踪所有子片段,当使用具有FragmentPagerAdapter的ViewPager时,这可能很麻烦。

由于我在许多地方都使用子片段,因此我想出了这个高效且模块化的解决方案(因此可以轻松删除),以防止他们修复它并且不再需要此无操作动画。

您可以以许多方式实现此功能。我选择使用单例,并将其称为ChildFragmentAnimationManager。它基本上会根据其父代为我跟踪子片段,并在需要时对子项应用无操作动画。

public class ChildFragmentAnimationManager {

private static ChildFragmentAnimationManager instance = null;

private Map<Fragment, List<Fragment>> fragmentMap;

private ChildFragmentAnimationManager() {
    fragmentMap = new HashMap<Fragment, List<Fragment>>();
}

public static ChildFragmentAnimationManager instance() {
    if (instance == null) {
        instance = new ChildFragmentAnimationManager();
    }
    return instance;
}

public FragmentTransaction animate(FragmentTransaction ft, Fragment parent) {
    List<Fragment> children = getChildren(parent);

    ft.setCustomAnimations(R.anim.no_anim, R.anim.no_anim, R.anim.no_anim, R.anim.no_anim);
    for (Fragment child : children) {
        ft.remove(child);
    }

    return ft;
}

public void putChild(Fragment parent, Fragment child) {
    List<Fragment> children = getChildren(parent);
    children.add(child);
}

public void removeChild(Fragment parent, Fragment child) {
    List<Fragment> children = getChildren(parent);
    children.remove(child);
}

private List<Fragment> getChildren(Fragment parent) {
    List<Fragment> children;

    if ( fragmentMap.containsKey(parent) ) {
        children = fragmentMap.get(parent);
    } else {
        children = new ArrayList<Fragment>(3);
        fragmentMap.put(parent, children);
    }

    return children;
}

}

接下来,您需要有一个扩展Fragment的类,所有的Fragment(至少是子Fragment)都要继承它。我已经有了这个类,并称其为BaseFragment。当一个Fragment的视图被创建时,我们将其添加到ChildFragmentAnimationManager中,在其被销毁时将其删除。您可以在onAttach/onDetach或其他匹配方法中完成此操作。我选择使用Create/Destroy View的逻辑是因为如果一个Fragment没有视图,我不关心是否对其进行动画处理以保持可见。这种方法也应该更好地与使用Fragment作为ViewPager的组件一起使用,因为您无需跟踪FragmentPagerAdapter正在持有的每个单独的Fragment,而只需要跟踪三个即可。

public abstract class BaseFragment extends Fragment {

@Override
public  View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {

    Fragment parent = getParentFragment();
    if (parent != null) {
        ChildFragmentAnimationManager.instance().putChild(parent, this);
    }

    return super.onCreateView(inflater, container, savedInstanceState);
}

@Override
public void onDestroyView() {
    Fragment parent = getParentFragment();
    if (parent != null) {
        ChildFragmentAnimationManager.instance().removeChild(parent, this);
    }

    super.onDestroyView();
}

}

现在所有的片段都由父片段存储在内存中,您可以像这样调用它们的动画效果,您的子片段将不会消失。

FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
ChildFragmentAnimationManager.instance().animate(ft, ReaderFragment.this)
                    .setCustomAnimations(R.anim.up_in, R.anim.up_out, R.anim.down_in, R.anim.down_out)
                    .replace(R.id.container, f)
                    .addToBackStack(null)
                    .commit();

此外,为了让您拥有它,这是no_anim.xml文件,应放置在您的res/anim文件夹中:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/linear_interpolator">
    <translate android:fromXDelta="0" android:toXDelta="0"
        android:duration="1000" />
</set>

再次强调,我认为这个解决方案并不完美,但它比每个实例都有一个子片段,在父片段中实现自定义代码来跟踪每个子片段要好得多。我曾经遇到过这种情况,那真的很烦人。


1
我认为我找到了比Luksprog建议的将当前片段快照成位图更好的解决方案。
诀窍是隐藏即将被移除或分离的片段,只有在动画完成后,在其自身的片段事务中才会移除或分离该片段。
想象一下我们有FragmentAFragmentB,两者都有子片段。现在当您通常这样做时:
getSupportFragmentManager()
  .beginTransaction()
  .setCustomAnimations(anim1, anim2, anim1, anim2)
  .add(R.id.fragmentHolder, new FragmentB())
  .remove(fragmentA)    <-------------------------------------------
  .addToBackStack(null)
  .commit()

相反,你要做的是

getSupportFragmentManager()
  .beginTransaction()
  .setCustomAnimations(anim1, anim2, anim1, anim2)
  .add(R.id.fragmentHolder, new FragmentB())
  .hide(fragmentA)    <---------------------------------------------
  .addToBackStack(null)
  .commit()

fragmentA.removeMe = true;

现在来实现Fragment:
public class BaseFragment extends Fragment {

    protected Boolean detachMe = false;
    protected Boolean removeMe = false;

    @Override
    public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
        if (nextAnim == 0) {
            if (!enter) {
                onExit();
            }

            return null;
        }

        Animation animation = AnimationUtils.loadAnimation(getActivity(), nextAnim);
        assert animation != null;

        if (!enter) {
            animation.setAnimationListener(new Animation.AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    onExit();
                }

                @Override
                public void onAnimationRepeat(Animation animation) {
                }
            });
        }

        return animation;
    }

    private void onExit() {
        if (!detachMe && !removeMe) {
            return;
        }

        FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
        if (detachMe) {
            fragmentTransaction.detach(this);
            detachMe = false;
        } else if (removeMe) {
            fragmentTransaction.remove(this);
            removeMe = false;
        }
        fragmentTransaction.commit();
    }
}

popBackStack会不会导致错误,因为它试图显示已分离的Fragment? - Alexandre

0
从@kcoppock的上面的答案中,
如果您有Activity->Fragment->Fragments(多重堆栈,以下有所帮助),在我看来,对最佳答案进行了轻微编辑。
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {

    final Fragment parent = getParentFragment();

    Fragment parentOfParent = null;

    if( parent!=null ) {
        parentOfParent = parent.getParentFragment();
    }

    if( !enter && parent != null && parentOfParent!=null && parentOfParent.isRemoving()){
        Animation doNothingAnim = new AlphaAnimation(1, 1);
        doNothingAnim.setDuration(getNextAnimationDuration(parent, DEFAULT_CHILD_ANIMATION_DURATION));
        return doNothingAnim;
    } else
    if (!enter && parent != null && parent.isRemoving()) {
        // This is a workaround for the bug where child fragments disappear when
        // the parent is removed (as all children are first removed from the parent)
        // See https://code.google.com/p/android/issues/detail?id=55228
        Animation doNothingAnim = new AlphaAnimation(1, 1);
        doNothingAnim.setDuration(getNextAnimationDuration(parent, DEFAULT_CHILD_ANIMATION_DURATION));
        return doNothingAnim;
    } else {
        return super.onCreateAnimation(transit, enter, nextAnim);
    }
}

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