Nested fragments disappear during transition anima

2020-01-26 12:48发布

Here's the scenario: Activity contains fragment A, which in turn uses getChildFragmentManager() to add fragments A1 and A2 in its onCreate like so:

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

So far, so good, everything is running as expected.

We then run the following transaction in the Activity:

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

During the transition, the enter animations for fragment B runs correctly but fragments A1 and A2 disappear entirely. When we revert the transaction with the Back button, they initialize properly and display normally during the popEnter animation.

In my brief testing, it got weirder - if I set the animations for the child fragments (see below), the exit animation runs intermittently when we add fragment B

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

The effect I want to achieve is simple - I want the exit (or should it be popExit?) animation on fragment A (anim2) to run, animating the whole container, including its nested children.

Is there any way to achieve that?

Edit: Please find a test case here

Edit2: Thanks to @StevenByle for pushing me to keep trying with the static animations. Apparently you can set animations on a per-op basis (not global to the whole transaction), which means the children can have an indefinite static animation set, while their parent can have a different animation and the whole thing can be committed in one transaction. See the discussion below and the updated test case project.

16条回答
相关推荐>>
2楼-- · 2020-01-26 13:30

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

查看更多
Summer. ? 凉城
3楼-- · 2020-01-26 13:30

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

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.

查看更多
Deceive 欺骗
4楼-- · 2020-01-26 13:31

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();
    }
}
查看更多
戒情不戒烟
5楼-- · 2020-01-26 13:32

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);
}
查看更多
小情绪 Triste *
6楼-- · 2020-01-26 13:33

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.

查看更多
Juvenile、少年°
7楼-- · 2020-01-26 13:34

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

查看更多
登录 后发表回答