I am trying to achieve the following effect using FragmentTransaction.setCustomAnimations.
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);
}
}
}
}
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 Fragment
s 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.
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:
androidx.fragment:fragment:1.2.0
;<fragment>
, <FrameLayout>
, or else) by <androidx.fragment.app.FragmentContainerView>
;FragmentTransaction.setCustomAnimations()
to animate your fragments transitions.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;
}