tinmegali
2/21/2016 - 7:12 PM

Add a circular Reveal and Unreveal transition animation to a Android Fragment

Add a circular Reveal and Unreveal transition animation to a Android Fragment

import android.animation.Animator;
import android.animation.TimeInterpolator;
import android.annotation.TargetApi;
import android.graphics.Color;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.DecelerateInterpolator;

import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collections;

/**
 * Created by Tin Megali on 21/02/16.
 * https://github.com/tinmegali
 *
 * based on https://gist.github.com/ferdy182/d9b3525aa65b5b4c468a
 *
 * Add a Reveal/UnReveal circular transition animation effect to a fragment.
 * It's possible to personalize:
 *  - BG color {@linkplain Builder#setRevealColor(int)}
 *  - Animation time {@linkplain Builder#setRevealTime(int)} {@linkplain Builder#setUnrevealTime(int)}
 *  - Interpolator {@linkplain Builder#setRevelInt(TimeInterpolator)} {@linkplain Builder#setUnrevealInt(TimeInterpolator)}
 *  - Choose to use or not a onTouch un reveal {@linkplain Builder#setOnToouchUnreveal(boolean)}
 *
 * Using:
 *  1 - Build and personalize an instance using
 *      {@link Builder}
 *  2 - To begin revealing
 *      {@linkplain #startReveal(int, int, OnCircularReveal)}
 *  3 - To terminate
 *      {@linkplain #startUnreveal(int, int, OnCircularReveal)}
 *  4 - Events will be sent using
 *      {@link OnCircularReveal}
 *
 *  IMPORTANT: CircularFragReveal won't exclude the fragment.
 *      You'll need to call this action after the animation ends
 *      using it's events
 *      {@link OnCircularReveal#onFragCircRevealStart()}
 *      {@link OnCircularReveal#onFragCircRevealEnded()}
 *      {@link OnCircularReveal#onFragCircUnRevealStart()}
 *      {@link OnCircularReveal#onFragCircUnRevealEnded()}
 *
 */
public class CircularFragReveal {

    private static final String TAG = CircularFragReveal.class.getSimpleName();

    private int mRevealDuration;
    private int mUnrevealDuration;
    private TimeInterpolator mRevealInterpolator, mUnrevealInterpolator;
    private Animator mRevealAnimator;
    private Animator mUnrevealAnimator;

    private WeakReference<View> mRootView;
    private int mRevealColor;
    private boolean mUseOnTouchUnreveal;
    private boolean mIsRevealing, mIsUnrevealing;


    private WeakReference<OnCircularReveal> mCallback;

    public interface OnCircularReveal {
        void onFragCircRevealStart();
        void onFragCircRevealEnded();
        void onFragCircUnRevealStart();
        void onFragCircUnRevealEnded();

    }

    /**
     * Constructor
     * Uses a Builder
     */
    private CircularFragReveal(Builder builder) {
        mRootView = new WeakReference<>(builder.getRootView());
        mRevealDuration = builder.getREVEAL_DURATION();
        mUnrevealDuration = builder.getUNREVEAL_DURATION();
        mRevealInterpolator = builder.getRevealInterpolator();
        mUnrevealInterpolator = builder.getUnrevealInterpolator();
        mRevealColor = builder.getRevealBgColor();
        mUseOnTouchUnreveal = builder.isTouchUnreveal();
    }

    /**
     * Start Circular unReveal animation
     *
     * @param centerX       Animation Center X
     * @param centerY       Animation Center Y
     * @param listener Unreveal listener
     */
    public void startReveal(int centerX, int centerY, @NonNull OnCircularReveal listener) {
        Log.d(TAG, "startReveal(centerX["+centerX+"], centerY["+centerY+"], callback["+listener+"])");
        mCallback = new WeakReference<>(listener);
        startRevealAnimation(centerX, centerY);
    }

    /**
     * Cancel Reveal animator
     */
    public void cancelReveal() {
        Log.d(TAG, "cencelReveal");
        mRevealAnimator.cancel();
    }

    /**
     * Current animation state
     */
    public boolean isRevealing() { return mIsRevealing; }
    public boolean isUnrevealing() { return mIsUnrevealing; }


    /**
     * Start Circular unReveal animation
     *
     * @param centerX       Animation Center X
     * @param centerY       Animation Center Y
     * @param listener Unreveal listener
     */
    public void startUnreveal(int centerX, int centerY, OnCircularReveal listener) {
        Log.d(TAG, "startUnreveal(cx[" + centerX + "], cy[" + centerY + "])");
        mCallback = new WeakReference<>(listener);
        try {
            mUnrevealAnimator = prepareUnrevealAnimator(centerX, centerY);
            mUnrevealAnimator.start();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Cancel unReveal animator
     */
    public void cancelUnreveal() {
        Log.d(TAG, "cancelUnreveal()");
        mUnrevealAnimator.cancel();
    }


    /**
     * Circular Reveal Builder
     */
    public static class Builder {

        private final String TAG = Builder.class.getSimpleName();

        private int REVEAL_DURATION = 200;
        private int UNREVEAL_DURATION = 200;
        private TimeInterpolator revealInterpolator;
        private TimeInterpolator unrevealInterpolator;
        private View rootView;
        private int revealBgColor;
        private boolean touchUnreveal;

        public Builder(View rootView) {
            this.rootView = rootView;
            this.revealInterpolator = new DecelerateInterpolator(2f);
            this.unrevealInterpolator = new AnticipateInterpolator(2f);
            this.revealBgColor = Color.parseColor("#e9fafafa");
        }

        public Builder setRevealTime(int duration) {
            Log.d(TAG, "setRevealTime(duration[" + duration + "])");
            this.REVEAL_DURATION = duration;
            return this;
        }

        public Builder setUnrevealTime(int duration) {
            Log.d(TAG, "setUnrevealTime(duration[" + duration + "])");
            this.UNREVEAL_DURATION = duration;
            return this;
        }

        public Builder setRevelInt(TimeInterpolator interpolator) {
            Log.d(TAG, "setRevelInt(interpolator[" + interpolator + "])");
            this.revealInterpolator = interpolator;
            return this;
        }

        public Builder setUnrevealInt(TimeInterpolator interpolator) {
            Log.d(TAG, "setUnrevealInt(interpolator[" + interpolator + "])");
            this.unrevealInterpolator = interpolator;
            return this;
        }

        public Builder setRevealColor(int color) {
            Log.d(TAG, "setRevealColor(color[" + color + "])");
            this.revealBgColor = color;
            return this;
        }

        public Builder setOnToouchUnreveal(boolean onTouchOn) {
            Log.d(TAG, "setOnToouchUnreveal(onTouch[" + onTouchOn + "])");
            this.touchUnreveal = onTouchOn;
            return this;
        }


        /**
         * Builder method
         */
        public CircularFragReveal build() {
            Log.d(TAG, "build()");
            return new CircularFragReveal(this);
        }

        public View getRootView() {
            return rootView;
        }

        public int getREVEAL_DURATION() {
            return REVEAL_DURATION;
        }

        public TimeInterpolator getRevealInterpolator() {
            return revealInterpolator;
        }

        public int getUNREVEAL_DURATION() {
            return UNREVEAL_DURATION;
        }

        public TimeInterpolator getUnrevealInterpolator() {
            return unrevealInterpolator;
        }

        public int getRevealBgColor() {
            return revealBgColor;
        }

        public boolean isTouchUnreveal() {
            return touchUnreveal;
        }
    }


    /**
     * Configures and start Reveal animation
     */
    private void startRevealAnimation(final int cx, final int cy) {
        Log.d(TAG, "startRevealAnimation()");
        if (Build.VERSION.SDK_INT >= 21) {
            // define BG color
            getView().setBackgroundColor(mRevealColor);

            // To run the animation as soon as the view is layout in the view hierarchy we add this
            // listener and remove it
            // as soon as it runs to prevent multiple animations if the view changes bounds
            getView().addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
                @TargetApi(Build.VERSION_CODES.LOLLIPOP)
                @Override
                public void onLayoutChange(View v, int left, int top, int right, int bottom,
                                           int oldLeft, int oldTop, int oldRight, int oldBottom) {
                    Log.d(TAG, "startRevealAnimation() | onLayoutChange");
                    v.removeOnLayoutChangeListener(this);

                    int radius = (int) Math.hypot(right, bottom);

                    int duration = mRevealDuration;
                    mRevealAnimator = ViewAnimationUtils.createCircularReveal(v, cx, cy, 0, radius);
                    mRevealAnimator.setInterpolator(mRevealInterpolator);
                    mRevealAnimator.setDuration(duration);
                    mRevealAnimator.addListener(mRevealAnimListener);
                    mRevealAnimator.start();
                }
            });

        } else {
            Log.w(TAG, "Cannot run CircularReveal on this SDK version[" + Build.VERSION.SDK_INT + "]. " +
                    "CircularReveal run on version 21 and greater.");
        }
    }

    private View getView() {
        Log.d(TAG, "getView()");
        return mRootView.get();
    }


    /**
     * Prepare UnReveal animation
     */
    private Animator prepareUnrevealAnimator(float cx, float cy) throws IllegalAccessException {
        Log.d(TAG, "prepareUnrevealAnimator(cx[" + cx + "], cy[" + cy + "]");
        if (Build.VERSION.SDK_INT >= 21) {
            int radius = getEnclosingCircleRadius(getView(), (int) cx, (int) cy);
            @SuppressWarnings("unchecked")
            Animator animator = ViewAnimationUtils.createCircularReveal(getView(), (int) cx, (int) cy, radius, 0);
            animator.setInterpolator(mUnrevealInterpolator);
            animator.setDuration(mUnrevealDuration);
            animator.addListener(mUnRevealAnimListener);
            return animator;
        } else
            throw new IllegalAccessException("Cannot prepare UnReveal with version[" + Build.VERSION.SDK_INT + "]");
    }

    private int getEnclosingCircleRadius(View v, int cx, int cy) {
        Log.d(TAG, "getEnclosingCircleRadius(view[" + v.getId() + "], cx[" + cx + "], cy[" + cy + "])");
        int realCenterX = cx + v.getLeft();
        int realCenterY = cy + v.getTop();
        int distTopLeft = (int) Math.hypot(realCenterX - v.getLeft(), realCenterY - v.getTop());
        int distTopRight = (int) Math.hypot(v.getRight() - realCenterX, realCenterY - v.getTop());
        int distBottomLeft = (int) Math.hypot(realCenterX - v.getLeft(), v.getBottom() - realCenterY);
        int distBottomRIght = (int) Math.hypot(v.getRight() - v.getLeft(), v.getBottom() - realCenterY);

        Integer[] distances = new Integer[]{distTopLeft, distTopRight, distBottomLeft, distBottomRIght};
        return Collections.max(Arrays.asList(distances));
    }


    /**
     * OnTouch UnReveal Listener
     */
    private View.OnTouchListener mRootTouchListener = new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            Log.d(TAG, "onTouch(view[" + v.getId() + "], event[" + event.getAction() + "])");
            getView().setOnTouchListener(null);
            startUnreveal((int)event.getX(), (int)event.getY(), mCallback.get());
            return true;
        }
    };
    /**
     * Turns onTouch unReveal listener ON
     */
    public void onTouchUnRevealOn() {
        Log.d(TAG, "onTouchUnRevealON()");
        getView().setOnTouchListener(mRootTouchListener);
    }
    /**
     * Turns ontouch unReveal listener OFF
     */
    public void onTouchUnRevealOff(){
        Log.d(TAG, "onTouchUnRevealOFF()");
        getView().setOnTouchListener(null);
    }


    /**
     * UnReveal Animator Listener
     */
    private Animator.AnimatorListener mUnRevealAnimListener = new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            Log.d(TAG, "onFragCircUnRevealStart");
            mCallback.get().onFragCircUnRevealStart();
            onTouchUnRevealOff();
            mIsUnrevealing = true;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            Log.d(TAG, "onFragCircUnRevealEnded");
            getView().setVisibility(View.INVISIBLE);
            mCallback.get().onFragCircUnRevealEnded();
            mIsUnrevealing = false;
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            Log.d(TAG, "onAddFragUnReveal cancel");
            mIsUnrevealing = false;
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
            Log.d(TAG, "onAddFragUnReveal repeat");
        }
    };

    /**
     * Reveal Animator Listener
     */
    private Animator.AnimatorListener mRevealAnimListener = new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            Log.d(TAG, "onAnimationStart");
            mCallback.get().onFragCircRevealStart();
            mIsRevealing = true;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            Log.d(TAG, "onAnimationEnd");
            mCallback.get().onFragCircRevealEnded();
            mIsRevealing = false;
            if (mUseOnTouchUnreveal) {
                Log.d(TAG, "turning touchListener ON");
                onTouchUnRevealOn();
            }
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            Log.d(TAG, "onAnimationCancel");
            onTouchUnRevealOff();
            mIsRevealing = false;
        }

        @Override
        public void onAnimationRepeat(Animator animation) {
            Log.d(TAG, "onAnimationRepeat");
        }
    };

}