Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to achieve smooth fragment transaction animations on Android

I've mostly worked with iOS and have become accustomed to very smooth and fluent screen change animations. I am now working on an Android app and can't for the life of me get a fragment transaction to smoothly add/replace a fragment.

My set up is as follows: MainActivity has a FrameLayout for its xml which I immediately load FragmentA into on the MainActivity's OnCreate. My app then proceeds to replace the MainActivity's FrameLayout with FragmentB, FragmentC, ect. So my entire app has 1 activity.

On button clicks I call the following to add/replace the current fragment:

        getFragmentManager()
            .beginTransaction()
            .setCustomAnimations(R.animator.slide_in, android.R.animator.fade_out, android.R.animator.fade_in, R.animator.slide_out)
            .addToBackStack(null)
            .add(R.id.fragment_1, fragment)
            .commit();

slide_in.xml looks like (slide_out.xml is obviously the opposite):

 <?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
            android:interpolator="@android:interpolator/linear"
            android:propertyName="xFraction"
            android:valueType="floatType"
            android:valueFrom="1.0"
            android:valueTo="0"
            android:duration="@android:integer/config_shortAnimTime"
        />
</set>

Sliding in and out I'm animating xFraction which I've subclassed LinearLayout to do like so:

public class SlidingLinearLayout extends LinearLayout {

    private float yFraction = 0;
    private float xFraction = 0;

    public SlidingLinearLayout(Context context) {
        super(context);
    }

    public SlidingLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    private ViewTreeObserver.OnPreDrawListener preDrawListener = null;

    public void setYFraction(float fraction) {

        this.yFraction = fraction;

        if (getHeight() == 0) {
            if (preDrawListener == null) {
                preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
                        setYFraction(yFraction);
                        return true;
                    }
                };
                getViewTreeObserver().addOnPreDrawListener(preDrawListener);
            }
            return;
        }

        float translationY = getHeight() * fraction;
        setTranslationY(translationY);
    }

    public float getYFraction() {
        return this.yFraction;
    }

    public void setXFraction(float fraction) {

        this.xFraction = fraction;

        if (getWidth() == 0) {
            if (preDrawListener == null) {
                preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
                        setXFraction(xFraction);
                        return true;
                    }
                };
                getViewTreeObserver().addOnPreDrawListener(preDrawListener);
            }
            return;
        }

        float translationX = getWidth() * fraction;
        setTranslationX(translationX);
    }

    public float getxFraction() {
        return xFraction;
    }
}

So my issue is most of the time when I initially click a button to add/replace a fragment, I don't get an animation it just sort of pops into place. However more times than not when I press the back button and the fragment pops from the backstack the animation executes as expected. I feel like it is an issue with initialization while trying to animate. When the fragment is in memory and animated off screen it does so much better than when a new fragment is created then animated on to the screen. I'm curious if others have experienced this and if so how they resolved it.

This is an annoyance that has plagued my Android development experience that I'd really like to put to rest! Any help would be greatly appreciated. Thanks!

like image 546
kev Avatar asked May 22 '15 20:05

kev


1 Answers

In my experience, if the fragment is doing too much work on load, then the system will basically skip the enter animation and just display the fragment. In my case the solution was to create an animation listener in onCreateAnimation to wait until the enter animation had finished before doing the process intensive work that was causing the animation to be skipped.

@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
    if (nextAnim == 0) {
        return super.onCreateAnimation(transit, enter, nextAnim);
    }

    Animation anim = android.view.animation.AnimationUtils.loadAnimation(getContext(), nextAnim);
    anim.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {}
        @Override
        public void onAnimationEnd(Animation animation) {
            // Do any process intensive work that can wait until after fragment has loaded
        }

        @Override
        public void onAnimationRepeat(Animation animation) {}
    });
    return anim;
}
like image 172
aaronmarino Avatar answered Nov 15 '22 17:11

aaronmarino