101

这是场景: Activity 包含 fragment ,它A反过来用于getChildFragmentManager()添加片段A1,如下所示:A2onCreate

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

到目前为止,一切都很好,一切都按预期运行。

然后我们在 Activity 中运行以下事务:

getSupportFragmentManager()
  .beginTransaction()
  .setCustomAnimations(anim1, anim2, anim1, anim2)
  .replace(R.id.fragmentHolder, new FragmentB())
  .addToBackStack(null)
  .commit()

在过渡期间,enter片段的动画B运行正常,但片段 A1 和 A2 完全消失popEnter当我们使用 Back 按钮恢复事务时,它们会正确初始化并在动画期间正常显示。

在我的简短测试中,它变得更奇怪了——如果我为子片段设置动画(见下文),exit当我们添加片段时动画会间歇性地运行B

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

我想要实现的效果很简单——我想要exit(或者应该是popExit?)片段A(anim2)上的动画运行,为整个容器设置动画,包括它的嵌套子级。

有没有办法做到这一点?

编辑:请在这里找到一个测试用例

Edit2:感谢@StevenByle 推动我继续尝试静态动画。显然你可以在每个操作的基础上设置动画(不是全局的整个事务),这意味着孩子可以有一个无限的静态动画集,而他们的父母可以有不同的动画,整个事情可以在一个事务中提交. 请参阅下面的讨论和更新的测试用例项目

4

16 回答 16

69

所以似乎有很多不同的解决方法,但基于@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;
    }
}

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

此解决方案无需“猜测”父动画的持续时间(因此“什么都不做”动画与父动画的退出动画具有相同的持续时间),并允许您仍然对子片段执行自定义动画(例如,如果您用不同的动画重新交换子片段)。

于 2014-04-24T17:44:49.707 回答
36

为了避免用户在事务中删除/替换父片段时看到嵌套片段消失,您可以通过提供它们出现在屏幕上的图像来“模拟”仍然存在的那些片段。此图像将用作嵌套片段容器的背景,因此即使嵌套片段的视图消失,图像也会模拟它们的存在。此外,我不认为失去与嵌套片段视图的交互性是一个问题,因为我认为您不希望用户在它们刚刚被删除的过程中对它们采取行动(可能作为用户操作作为好)。

我做了一个设置背景图像的小例子(一些基本的东西)。

于 2013-02-22T05:53:28.477 回答
32

我能够想出一个非常干净的解决方案。IMO 它是最不老套的,虽然这在技术上是“绘制位图”解决方案,但至少它由片段库抽象。

确保您的孩子使用以下代码覆盖父类:

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);
}

如果我们在子片段上有一个退出动画,它们将被动画化而不是眨眼。我们可以通过一个动画来利用这一点,该动画只是在一段时间内以完整的 Alpha 绘制子片段。这样,它们将在父片段动画时保持可见,从而提供所需的行为。

我能想到的唯一问题是跟踪该持续时间。我也许可以将它设置为一个很大的数字,但如果它仍在某处绘制动画,我担心它可能会出现性能问题。

于 2014-03-26T21:54:37.777 回答
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>
于 2014-01-16T00:35:50.183 回答
7

我知道这可能无法完全解决您的问题,但也许它会适合其他人的需求,您可以为您的孩子添加enter/exitpopEnter/动画,而这些动画实际上不会移动/动画s。只要动画与其父动画具有相同的持续时间/偏移量,它们就会看起来与父动画一起移动/动画。popExitFragmentFragmentFragment

于 2013-02-21T14:40:34.607 回答
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);
}
于 2017-12-14T07:42:04.217 回答
2

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

编辑:我最终没有实现这个解决方案,因为它还有其他问题。Square 最近推出了 2 个替换片段的库。我想说这实际上可能是一个比试图破解片段做谷歌不希望他们做的事情更好的选择。

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

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

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

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

有很多方法可以实现这一点。我选择使用单例,我称之为 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。创建片段视图时,我们将其添加到 ChildFragmentAnimationManager,并在其销毁时将其删除。您可以在附加/分离或序列中的其他匹配方法上执行此操作。我选择 Create/Destroy View 的逻辑是因为如果 Fragment 没有 View,我不关心动画它以继续被看到。这种方法也应该更适用于使用 Fragment 的 ViewPager,因为您不会跟踪 FragmentPagerAdapter 持有的每一个 Fragment,而只会跟踪 3 个。

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();
}

}

现在你所有的 Fragment 都由父 Fragment 存储在内存中,你可以像这样对它们调用 animate ,你的子 Fragment 不会消失。

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();

另外,只要你有它,这里是你的 res/anim 文件夹中的 no_anim.xml 文件:

<?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>

同样,我不认为这个解决方案是完美的,但它比每个实例都有一个子片段要好得多,在父片段中实现自定义代码以跟踪每个子片段。我去过那里,它并不好玩。

于 2013-07-03T23:45:34.173 回答
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();
    }
}
于 2014-02-08T02:23:34.580 回答
1

我对地图片段有同样的问题。它在其包含的片段的退出动画期间不断消失。解决方法是为子地图片段添加动画,使其在父片段的退出动画期间保持可见。子片段的动画在其持续期间将其 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;
}

最后,在资源文件中设置子片段动画的持续时间。

值/整数.xml

<resources>
  <integer name="keep_child_fragment_animation_duration">500</integer>
</resources>
于 2016-01-25T05:59:57.663 回答
0

为了动画嵌套片段的消失,我们可以在 ChildFragmentManager 上强制弹出堆栈。这将触发过渡动画。为此,我们需要赶上 OnBackButtonPressed 事件或监听 backstack 更改。

这是带有代码的示例。

View.OnClickListener() {//this is from custom button but you can listen for back button pressed
            @Override
            public void onClick(View v) {
                getChildFragmentManager().popBackStack();
                //and here we can manage other fragment operations 
            }
        });

  Fragment fr = MyNeastedFragment.newInstance(product);

  getChildFragmentManager()
          .beginTransaction()
                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
                .replace(R.neasted_fragment_container, fr)
                .addToBackStack("Neasted Fragment")
                .commit();
于 2017-04-21T13:47:24.307 回答
0

我最近在我的问题中遇到了这个问题:嵌套片段转换不正确

我有一个解决方案,可以在不保存位图、不使用反射或任何其他不满意的方法的情况下解决这个问题。

可以在此处查看示例项目:https ://github.com/zafrani/NestedFragmentTransitions

可以在此处查看效果的 GIF:https ://imgur.com/94AvrW4

在我的示例中,有 6 个子片段,分为两个父片段。我能够毫无问题地实现进入、退出、弹出和推送的转换。配置更改和后压也成功处理。

大部分解决方案在我的 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()
}

活动和父片段负责设置这些布尔值的状态。从我的示例项目中查看方式和位置更容易。

在我的示例中,我没有使用支持片段,但可以对它们及其 onCreateAnimation 函数使用相同的逻辑

于 2017-10-24T00:17:43.110 回答
0

解决这个问题的一个简单方法是使用Fragment这个库中的类而不是标准库片段类:

https://github.com/marksalpeter/contract-fragment

作为旁注,该包还包含一个有用的委托模式,称为ContractFragment您可能会发现它对利用父子片段关系构建应用程序很有用。

于 2018-02-13T23:53:11.337 回答
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);
    }
}
于 2018-06-06T19:07:36.843 回答
0

我的问题是父片段删除(ft.remove(fragment)),子动画没有发生。

基本问题是子片段在父片段退出动画之前立即被销毁。

子片段自定义动画不会在父片段移除时执行

正如其他人所逃避的那样,在移除 PARENT 之前隐藏 PARENT(而不是孩子)是要走的路。

            val ft = fragmentManager?.beginTransaction()
            ft?.setCustomAnimations(R.anim.enter_from_right,
                    R.anim.exit_to_right)
            if (parentFragment.isHidden()) {
                ft?.show(vehicleModule)
            } else {
                ft?.hide(vehicleModule)
            }
            ft?.commit()

如果您确实想删除父级,您可能应该在自定义动画上设置一个侦听器以了解动画何时结束,这样您就可以安全地对父级片段进行一些最终确定(删除)。如果您不及时执行此操作,您最终可能会终止动画。NB 动画是在它自己的异步队列上完成的。

顺便说一句,您不需要子片段上的自定义动画,因为它们将继承父动画。

于 2019-03-13T15:53:50.927 回答
0

该问题已在androidx.fragment:fragment:1.2.0-alpha02. 有关详细信息,请参阅https://issuetracker.google.com/issues/116675313 。

于 2019-08-14T19:12:09.620 回答
0

旧线程,但万一有人在这里绊倒:

上述所有方法对我来说都没有吸引力,位图解决方案非常肮脏且性能不佳;其他的要求子片段了解用于创建相关子片段的事务中使用的转换的持续时间。在我看来,一个更好的解决方案类似于以下内容:

val currentFragment = supportFragmentManager.findFragmentByTag(TAG)
val transaction = supportFragmentManager
    .beginTransaction()
    .setCustomAnimations(anim1, anim2, anim1, anim2)
    .add(R.id.fragmentHolder, FragmentB(), TAG)
if (currentFragment != null) {
    transaction.hide(currentFragment).commit()
    Handler().postDelayed({
        supportFragmentManager.beginTransaction().remove(currentFragment).commit()
    }, DURATION_OF_ANIM)
} else {
    transaction.commit()
}

我们只是隐藏当前片段并添加新片段,当动画完成后我们删除旧片段。这样,它在一个地方处理,并且不创建位图。

于 2019-10-26T11:48:38.517 回答