FragmentTransaction animation to slide in over top

I am trying to achieve the following effect using FragmentTransaction.setCustomAnimations.

  1. Fragment A is showing
  2. Replace Fragment A with Fragment B. Fragment A should remain visible during the replacement. Fragment B should slide in from the right. Fragment B should slide in OVER THE TOP of Fragment A.

I have no problem getting the slide in animation setup. My problem is that I cannot figure out how to make Fragment A stay where it is and be UNDER Fragment B while the slide in animation is running. No matter what I do it seems that Fragment A is on top.

How can I achieve this?

Here is the FragmentTransaction code:

FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(R.anim.slide_in_right, R.anim.nothing, R.anim.nothing,
R.anim.slide_out_right);
ft.replace(R.id.fragment_content, fragment, name);
ft.addToBackStack(name);
ft.commit();

As you can see I have defined an animation R.anim.nothing for the "out" animation because I actually don't want Fragment A to do anything other than just stay where it is during the transaction.

Here are the animation resources:

slide_in_right.xml

<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="100%p"
android:toXDelta="0"
android:zAdjustment="top" />

nothing.xml

<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@android:integer/config_mediumAnimTime"
android:fromAlpha="1.0"
android:toAlpha="1.0"
android:zAdjustment="bottom" />
54570 次浏览

I don't know if you still need an answer but I recently needed to do the same and I found a way to do what you want.

I made something like this :

FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();


MyFragment next = getMyFragment();


ft.add(R.id.MyLayout,next);
ft.setCustomAnimations(R.anim.slide_in_right,0);
ft.show(next);
ft.commit();

I display my Fragment in a FrameLayout.

It work fines but the older Fragment is still in my View, I let android manage it like he wants because if I put:

ft.remove(myolderFrag);

it is not displayed during the animation.

slide_in_right.xml

    <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate android:duration="150" android:fromXDelta="100%p"
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="0" />
</set>

I found a solution which works for me. I ended up using a ViewPager with a FragmentStatePagerAdapter. The ViewPager provides the swiping behavior and the FragmentStatePagerAdapter swaps in the fragments. The final trick to achieve the effect of having one page visible "under" the incoming page is to use a PageTransformer. The PageTransformer overrides the ViewPager's default transition between pages. Here is an example PageTransformer that achieves the effect with translation and a small amount of scaling on the left-hand side page.

public class ScalePageTransformer implements PageTransformer {
private static final float SCALE_FACTOR = 0.95f;


private final ViewPager mViewPager;


public ScalePageTransformer(ViewPager viewPager) {
this.mViewPager = viewPager;
}


@SuppressLint("NewApi")
@Override
public void transformPage(View page, float position) {
if (position <= 0) {
// apply zoom effect and offset translation only for pages to
// the left
final float transformValue = Math.abs(Math.abs(position) - 1) * (1.0f - SCALE_FACTOR) + SCALE_FACTOR;
int pageWidth = mViewPager.getWidth();
final float translateValue = position * -pageWidth;
page.setScaleX(transformValue);
page.setScaleY(transformValue);
if (translateValue > -pageWidth) {
page.setTranslationX(translateValue);
} else {
page.setTranslationX(0);
}
}
}


}

I found an alternate solution (not heavily tested) that I find more elegant than the proposals so far:

final IncomingFragment newFrag = new IncomingFragment();
newFrag.setEnterAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {


}


@Override
public void onAnimationEnd(Animation animation) {
clearSelection();
inFrag.clearEnterAnimationListener();


getFragmentManager().beginTransaction().remove(OutgoingFragment.this).commit();
}


@Override
public void onAnimationRepeat(Animation animation) {


}
});


getActivity().getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_in_from_right, 0)
.add(R.id.container, inFrag)
.addToBackStack(null)
.commit();

This is being called from within an inner class of the OutgoingFragment class.

A new fragment is being inserted, the animation completes, then the old fragment is being removed.

There may be some memory problems with this in some applications but it is better than retaining both fragments indefinitely.

After more experimentation (hence this is my second answer), the problem seems to be that R.anim.nothing means 'disappear' when we want another animation that explicitly says 'stay put.' The solution is to define a true 'do nothing' animation like this:

Make file no_animation.xml:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:interpolator="@android:anim/linear_interpolator"
android:fromXScale="1.0"
android:toXScale="1.0"
android:fromYScale="1.0"
android:toYScale="1.0"
android:duration="200"
/>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="1.0"
android:toAlpha="0.0"
android:duration="200"
android:startOffset="200"
/>
</set>

Now simply do as you would otherwise:

getActivity().getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_in_right, R.anim.no_animation)
.replace(R.id.container, inFrag, FRAGMENT_TAG)
.addToBackStack("Some text")
.commit();

Update (June 16, 2020)

Starting from fragment library 1.2.0 the recommanded way to fix this issue is to use FragmentContainerView with FragmentTransaction.setCustomAnimations().

According to the documentation:

Fragments using exit animations are drawn before all others for FragmentContainerView. This ensures that exiting Fragments do not appear on top of the view.

Steps to fix this issue are:

  1. Update fragment library to 1.2.0 or more androidx.fragment:fragment:1.2.0;
  2. Replace your xml fragment container (<fragment>, <FrameLayout>, or else) by <androidx.fragment.app.FragmentContainerView>;
  3. Use FragmentTransaction.setCustomAnimations() to animate your fragments transitions.

Previous answer (Nov 19, 2015)

Starting from Lollipop, you can increase de translationZ of your entering fragment. It will appear above the exiting one.

For example:

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ViewCompat.setTranslationZ(getView(), 100.f);
}

If you want to modify the translationZ value only for the duration of the animation, you should do something like this:

@Override
public Animation onCreateAnimation(int transit, final boolean enter, int nextAnim) {
Animation nextAnimation = AnimationUtils.loadAnimation(getContext(), nextAnim);
nextAnimation.setAnimationListener(new Animation.AnimationListener() {


private float mOldTranslationZ;


@Override
public void onAnimationStart(Animation animation) {
if (getView() != null && enter) {
mOldTranslationZ = ViewCompat.getTranslationZ(getView());
ViewCompat.setTranslationZ(getView(), 100.f);
}
}


@Override
public void onAnimationEnd(Animation animation) {
if (getView() != null && enter) {
ViewCompat.setTranslationZ(getView(), mOldTranslationZ);
}
}


@Override
public void onAnimationRepeat(Animation animation) {
}
});
return nextAnimation;
}
FragmentTransaction ft = ((AppCompatActivity) context).getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(0, R.anim.slide_out_to_right);
if (!fragment.isAdded())
{
ft.add(R.id.fragmentContainerFrameMyOrders, fragment);
ft.show(fragment);
}
else
ft.replace(R.id.fragmentContainerFrameMyOrders, fragment);
ft.commit();

This is my current workaround for anybody interested.

In the function for adding the new Fragment:

final Fragment toRemove = fragmentManager.findFragmentById(containerID);
if (toRemove != null) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
fragmentManager.beginTransaction().hide(toRemove).commit();
}
},
getResources().getInteger(android.R.integer.config_mediumAnimTime) + 100);
// Use whatever duration you chose for your animation for this handler
// I added an extra 100 ms because the first transaction wasn't always
// fast enough
}
fragmentManager.beginTransaction()
.setCustomAnimations(enter, 0, 0, popExit).add(containerID, fragmentToAdd)
.addToBackStack(tag).commit();

and in onCreate:

final FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.addOnBackStackChangedListener(
new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
Fragment current = fragmentManager.findFragmentById(containerID);
if (current != null && current.isHidden()) {
fragmentManager.beginTransaction().show(current).commit();
}
}
});

I would prefer some sort of AnimationListener instead of a Handler above, but I didn't see any way you could use one to detect the end of the transaction animation that wasn't tied to the fragment like onCreateAnimation(). Any suggestions/edits with an appropriate listener would be appreciated.

I will point out the Fragments I am adding this way are lightweight so it isn't a problem for me to have them in the fragment container along with the fragment they are on top of.

If you want to remove the fragment you could put fragmentManager.beginTransaction().remove(toRemove).commitAllowingStateLoss(); in the Handler's Runnable, and in the OnBackStackChangedListener:

// Use back stack entry tag to get the fragment
Fragment current = getCurrentFragment();
if (current != null && !current.isAdded()) {
fragmentManager.beginTransaction()
.add(containerId, current, current.getTag())
.commitNowAllowingStateLoss();
}

Note the above solution doesn't work for the first fragment in the container (because it isn't in the back stack) so you would have to have another way to restore that one, perhaps save a reference to the first fragment somehow... But if you don't use the back stack and always replace fragments manually, this is not an issue. OR you could add all fragments to the back stack (including the first one) and override onBackPressed to make sure your activity exits instead of showing a blank screen when only one fragment is left in the back stack.

EDIT: I discovered the following functions that could probably replace FragmentTransaction.remove() and FragmentTransaction.add() above:

FragmentTransaction.detach():

Detach the given fragment from the UI. This is the same state as when it is put on the back stack: the fragment is removed from the UI, however its state is still being actively managed by the fragment manager. When going into this state its view hierarchy is destroyed.

FragmentTransaction.attach():

Re-attach a fragment after it had previously been detached from the UI with detach(Fragment). This causes its view hierarchy to be re-created, attached to the UI, and displayed.

Based on jfrite answer attaching my implementations

import android.content.res.Resources;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewCompat;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.util.Log;


public final class AnimationHelper {
private AnimationHelper() {


}


private static String TAG = AnimationHelper.class.getSimpleName();
private static final float ELEVATION_WHILE_ENTER_ANIMATION_IS_RUNNING = 100f;
private static final int RESTORE_ANIMATION_DELAY = 16;


/**
* When replacing fragments with animations, by default the new fragment is placed below the replaced fragment. This
* method returns an animation object that sets high elevation at the beginning of the animation and resets the
* elevation when the animation completes. The {@link Animation} object that is returned is not the actual object
* that is used for the animating the fragment but the callbacks are called at the appropriate time. The method
* {@link Fragment#onCreateAnimation(int, boolean, int)} by default returns null, therefor, this method can be used
* as the return value for {@link Fragment#onCreateAnimation(int, boolean, int)} method although it can return
* null.
* @param enter True if fragment is 'entering'.
* @param nextAnim Animation resource id that is about to play.
* @param fragment The animated fragment.
* @return If nextAnim is a valid resource id and 'enter' is true, returns an {@link Animation} object with the
* described above behavior, otherwise returns null.
*/
@Nullable
public static Animation increaseElevationWhileAnimating(boolean enter, int nextAnim,
@NonNull Fragment fragment) {
if (!enter || nextAnim == 0) {
return null;
}
Animation nextAnimation;
try {
nextAnimation = AnimationUtils.loadAnimation(fragment.getContext(), nextAnim);
} catch (Resources.NotFoundException e) {
Log.e(TAG, "Can't find animation resource", e);
return null;
}
nextAnimation.setAnimationListener(new Animation.AnimationListener() {
private float oldTranslationZ;


@Override
public void onAnimationStart(Animation animation) {
if (fragment.getView() != null && !fragment.isDetached()) {
oldTranslationZ = ViewCompat.getTranslationZ(fragment.getView());
ViewCompat.setTranslationZ(fragment.getView(), ELEVATION_WHILE_ENTER_ANIMATION_IS_RUNNING);
}
}


@Override
public void onAnimationEnd(Animation animation) {
if (fragment.getView() != null && !fragment.isDetached()) {
fragment.getView().postDelayed(() -> {
// Decreasing the elevation at the ned can cause some flickering because of timing issues,
// Meaning that the replaced fragment did not complete yet the animation. Resting the animation
// with a minor delay solves the problem.
if (!fragment.isDetached()) {
ViewCompat.setTranslationZ(fragment.getView(), oldTranslationZ);
}
}, RESTORE_ANIMATION_DELAY);
}
}


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

Here is how I use the helper form the fragment.

@Override
public Animation onCreateAnimation(int transit, final boolean enter, int nextAnim) {
return AnimationHelper.increaseElevationWhileAnimating(enter, nextAnim, this);
}

Here is how i start the fragment with animation

FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.setCustomAnimations(R.anim.slide_in, R.anim.hold, R.anim.hold, R.anim.slide_out);

Add an elevation to the layout.

I added an elevation to the fragment that slides in from the bottom of 30sp and it worked.

I have tried many solutions suggested here. Here are the full code and output that combines all the idea + adding elevation.

Output:

Without elevation:

enter image description here

With elevation:

enter image description here

Full Code:

Add an elevation to the root

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="30sp">
----


----
</RelativeLayout>

How to slide in the fragment from the bottom?

getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(R.anim.slide_in_bottom, R.anim.do_nothing, R.anim.do_nothing, R.anim.slide_out_bottom)
.replace(R.id.fragmentContainer, currentFragment, "TAG")
.addToBackStack("TAG")
.commit();

How to do the reverse when the back button is pressed?

getSupportFragmentManager()
.popBackStack();

Since we have already defined enter and exit animation on setCustomAnimations() method. Calling popBackStack(); takes care of the reverse animation.

R.anim.slide_in_bottom

<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="500"
android:fromYDelta="100%"
android:toYDelta="0%">
</translate>
</set>

R.anim.slide_out_bottom

<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="500"
android:fromYDelta="0%"
android:toYDelta="100%">
</translate>
</set>

R.anim.do_nothing

<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale
android:interpolator="@android:anim/linear_interpolator"
android:fromXScale="1.0"
android:toXScale="1.0"
android:fromYScale="1.0"
android:toYScale="1.0"
android:duration="500"/>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="1.0"
android:toAlpha="1.0"
android:duration="500"
android:startOffset="500" />
</set>

Answer when using fragment manager

   getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(
R.anim.slide_in_from_left,
R.anim.slide_free_animation,
R.anim.slide_free_animation,
R.anim.slide_out_to_left
)
.replace(R.id.fragmentContainer, currentFragment, "TAG")
.addToBackStack("TAG")
.commit()

Answer when using Navigation:

     <action
android:id="@+id/action_firstFragment_to_secondFragment"
app:destination="@id/secondFragment"
app:enterAnim="@anim/slide_in_from_left"
app:exitAnim="@anim/slide_free_animation"
app:popEnterAnim="@anim/slide_free_animation"
app:popExitAnim="@anim/slide_out_to_left" />

slide_in_from_left.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">


<translate
android:duration="500"
android:fromXDelta="-100%p"
android:fromYDelta="0%"
android:interpolator="@android:anim/accelerate_interpolator"
android:toXDelta="0%"
android:toYDelta="0%" />


</set>


slide_out_to_left.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">


<translate
android:duration="500"
android:fromXDelta="0%"
android:fromYDelta="0%"
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="-100%p"
android:toYDelta="0%" />


</set>


slide_free_animation.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">


<alpha
android:duration="300"
android:fromAlpha="1.0"
android:startOffset="200"
android:toAlpha="0.0" />


</set>

Quite a lot of steps but was able to do this with a mix of transitions and animations

 supportFragmentManager
.beginTransaction()
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN
.setCustomAnimations(R.anim.nothing, R.anim.scale_out)
.replace(R.id.fragment_container, fragment)
.commit()

Nothing.xml

<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="@integer/transition_animation_time" />

Scale out - Animates old fragment on the way out

<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:fromXScale="100%"
android:fromYScale="100%"
android:toXScale="90%"
android:toYScale="90%"
android:pivotY="50%"
android:pivotX="50%"/>

Fragment.kotlin

   override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide)
}

res/transition/slide.xml - Animates new fragment on the way in

<?xml version="1.0" encoding="utf-8"?>
<slide xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="500"
android:slideEdge="bottom" />

I have the same problem, and all of the solutions looks like overkill. Hiding fragments lead to errors when you should remove fragment after animation finished, but you don't know when it happens.


I found simplest solution: just set for previous fragment negative translationZ in transaction:

childFragmentManager.fragments.lastOrNull()?.let { it.view?.translationZ = -1f }

Whole transaction looks like:

childFragmentManager.commit {
setCustomAnimations(...)
childFragmentManager.fragments.lastOrNull()?.let { it.view?.elevation = -1f }
replace(..., ...)
addToBackStack(...)
}

That's all.

It shouldn't affect any behavior of your views. Everything should works as usual. Animation to back also works correctly (with correct order).