嵌套片段在过渡动画期间消失

下面是场景: Activity 包含片段 A,它依次使用 getChildFragmentManager()在其 onCreate中添加片段 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()

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

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

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

我想要达到的效果很简单-我想要的 exit(或应该是 popExit?)动画在片段 A(anim2)上运行,动画整个容器,包括其嵌套的子容器。

有办法做到吗?

编辑 : 请在这里找到一个测试用例 < a href = “ https://github.com/BurntBrunch/NestedFragmentsAnimationsTest”>

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

25167 次浏览

I understand this may not be able to completely solve your problem, but maybe it will suit someone else's needs, you can add enter/exit and popEnter/popExit animations to your children Fragments that do not actually move/animate the Fragments. As long as the animations have the same duration/offset as their parent Fragment animations, they will appear to move/animate with the parent's animation.

In order to avoid the user seeing the nested fragments disappearing when the parent fragment is removed/replaced in a transaction you could "simulate" those fragments still being present by providing an image of them, as they appeared on the screen. This image will be used as a background for the nested fragments container so even if the views of the nested fragment go away the image will simulate their presence. Also, I don't see loosing the interactivity with the nested fragment's views as a problem because I don't think you would want the user to act on them when they are just in the process of being removed(probably as a user action as well).

I've made a little example with setting up the background image(something basic).

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

EDIT: I ended up unimplementing this solution as there were other problems that this has. Square recently came out with 2 libraries that replace fragments. I'd say this might actually be a better alternative than trying to hack fragments into doing something google doesn't want them doing.

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

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

I figured I'd put up this solution to help people who have this problem in the future. If you trace through the original posters conversation with other people, and look at the code he posted, you'll see the original poster eventually comes to the conclusion of using a no-op animation on the child fragments while animating the parent fragment. This solution isn't ideal as it forces you to keep track of all the child fragments, which can be cumbersome when using a ViewPager with FragmentPagerAdapter.

Since I use Child Fragments all over the place I came up with this solution that is efficient, and modular (so it can be easily removed) in case they ever fix it and this no-op animation is no longer needed.

There are lots of ways you can implement this. I chose to use a singleton, and I call it ChildFragmentAnimationManager. It basically will keep track of a child fragment for me based on its parent and will apply a no-op animation to the children when asked.

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


}

Next you need to have a class that extends Fragment that all your Fragments extend (at least your Child Fragments). I already had this class, and I call it BaseFragment. When a fragments view is created, we add it to the ChildFragmentAnimationManager, and remove it when it's destroyed. You could do this onAttach/Detach, or other matching methods in the sequence. My logic for choosing Create/Destroy View was because if a Fragment doesn't have a View, I don't care about animating it to continue to be seen. This approach should also work better with ViewPagers that use Fragments as you won't be keeping track of every single Fragment that a FragmentPagerAdapter is holding, but rather only 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();
}


}

Now that all your Fragments are stored in memory by the parent fragment, you can call animate on them like this, and your child fragments won't disappear.

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

Also, just so you have it, here is the no_anim.xml file that goes in your res/anim folder:

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

Again, I don't think this solution is perfect, but it's much better than for every instance you have a Child Fragment, implementing custom code in the parent fragment to keep track of each child. I've been there, and it's no fun.

Im posting my solution for clarity. The solution is quite simple. If you are trying to mimic the parent's fragment transaction animation just simply add a custom animation to the child fragment transaction with same duration. Oh and make sure you set the custom animation before add().

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

The xml for R.anim.none (My parents enter/exit animation time is 250ms)

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

I think i found a better solution to this problem than snapshotting the current fragment to a bitmap as Luksprog suggested.

The trick is to hide the fragment being removed or detached and only after the animations have been completed the fragment is removed or detached in its own fragment transaction.

Imagine we have FragmentA and FragmentB, both with sub fragments. Now when you would normally do:

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

Instead you do

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


fragmentA.removeMe = true;

Now for the implementation of the 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();
}
}

I was able to come up with a pretty clean solution. IMO its the least hacky, and while this is technically the "draw a bitmap" solution at least its abstracted by the fragment lib.

Make sure your child frags override a parent class with this:

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

If we have an exit animation on the child frags, they will be animated instead of blink away. We can exploit this by having an animation that simply draws the child fragments at full alpha for a duration. This way, they'll stay visible in the parent fragment as it animates, giving the desired behavior.

The only issue I can think of is keeping track of that duration. I could maybe set it to a large-ish number but I'm afraid that might have performance issues if its still drawing that animation somewhere.

So there seem to be a lot of different workarounds for this, but based on @Jayd16's answer, I think I've found a pretty solid catch-all solution that still allows for custom transition animations on child fragments, and doesn't require doing a bitmap cache of the layout.

Have a BaseFragment class that extends Fragment, and make all of your fragments extend that class (not just child fragments).

In that BaseFragment class, add the following:

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

It does, unfortunately, require reflection; however, since this workaround is for the support library, you don't run the risk of the underlying implementation changing unless you update your support library. If you're building the support library from source, you could add an accessor for the next animation resource ID to Fragment.java and remove the need for reflection.

This solution removes the need to "guess" the parent's animation duration (so that the "do nothing" animation will have the same duration as the parent's exit animation), and allows you to still do custom animations on child fragments (e.g. if you're swapping child fragments around with different animations).

I was having the same issue with map fragment. It kept disappearing during the exit animation of its containing fragment. The workaround is to add animation for the child map fragment which will keep it visible during the exit animation of the parent fragment. The animation of the child fragment is keeping its alpha at 100% during its duration period.

Animation: 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>

The animation is then applied when the map fragment is added to the parent fragment.

Parent fragment

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

Finally, the duration of the child fragment animation is set in a resource file.

values/integers.xml

<resources>
<integer name="keep_child_fragment_animation_duration">500</integer>
</resources>

To animate dissapearance of neasted fragments we can force pop back stack on ChildFragmentManager. This will fire transition animation. To do this we need to catch up OnBackButtonPressed event or listen for backstack changes.

Here is example with code.

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

I recently ran into this problem in my question: Nested fragments transitioning incorrectly

I have a solution that solves this without saving a bitmap, nor using reflection or any other unsatisfying methods.

An example project can be viewed here: https://github.com/zafrani/NestedFragmentTransitions

A GIF of the effect can be viewed here: https://imgur.com/94AvrW4

In my example there are 6 children fragments, split between two parent fragments. I'm able to achieve the transitions for enter, exit, pop and push without any problems. Configuration changes and back presses are also successfully handled.

The bulk of the solution is in my BaseFragment's (the fragment extended by my children and parent fragments) onCreateAnimator function which looks like this:

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

The activity and parent fragment are responsible for setting the states of these booleans. Its easier to view how and where from my example project.

I am not using support fragments in my example, but the same logic can be used with them and their onCreateAnimation function

you can do this in the child fragment.

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

A simple way to fix this problem is use the Fragment class from this library instead of the standard library fragment class:

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

As a side note, the package also contains a useful delegate pattern called ContractFragment that you might find useful for building your apps leveraging the parent-child fragment relationship.

From the above answer of @kcoppock,

if you have Activity->Fragment->Fragments ( multiple stacking, the following helps ), a minor edit to the best answer IMHO.

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

My problem was on parent fragment removal (ft.remove(fragment)), child animations were not happening.

The basic problem is that child fragments are immediately DESTROYED PRIOR to the parents fragment exiting animation.

Child fragments custom animations do not get executed on Parent Fragment removal

As others have eluded to, hiding the PARENT (and not the child) prior to PARENT removal is the way to go.

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

If you actually want to remove the parent you should probably set up a listener on you custom animation to know when the animation is ended, so then you can safely do some finalisation on the Parent Fragment (remove). If you don't do this, in a timely fashion, you could end up killing the animation. N.B animation is done on asynchronous queue of its own.

BTW you don't need custom animations on the child fragment, as they will inherit the parent animations.

The issue is fixed in androidx.fragment:fragment:1.2.0-alpha02. See https://issuetracker.google.com/issues/116675313 for more details.

Old thread, but in case someone stumbles in here:

All of the approaches above feel very unappealing for me, the bitmap solution is very dirty and non performant; the other ones require the child fragments to know about the duration of the transition used in the transaction used to create the child fragment in question. A better solution in my eyes i something like the following:

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

We just hide the current fragment and add the new fragment, when the animation has finished we remove the old fragment. This way it is handled in one place and no bitmap is created.