/* * Copyright (C) 2015 Naman Dwivedi * * Licensed under the GNU General Public License v3 * * This is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. */ package com.naman14.timber.transition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Path; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.transition.Transition; import android.transition.TransitionValues; import android.util.ArrayMap; import android.util.AttributeSet; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; import com.naman14.timber.R; import java.util.ArrayList; @TargetApi(21) public class PlayTransition extends Transition { private static final String PROPERTY_BOUNDS = "circleTransition:bounds"; private static final String PROPERTY_POSITION = "circleTransition:position"; private static final String PROPERTY_IMAGE = "circleTransition:image"; private static final String[] TRANSITION_PROPERTIES = { PROPERTY_BOUNDS, PROPERTY_POSITION, }; private int mColor = Color.parseColor("#6c1622"); public PlayTransition() { } public PlayTransition(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PlayTransition); setColor(a.getColor(R.styleable.PlayTransition_colorCT, getColor())); a.recycle(); } public void setColor(int color) { mColor = color; } public int getColor() { return mColor; } @Override public String[] getTransitionProperties() { return TRANSITION_PROPERTIES; } private void captureValues(TransitionValues transitionValues) { final View view = transitionValues.view; transitionValues.values.put(PROPERTY_BOUNDS, new Rect( view.getLeft(), view.getTop(), view.getRight(), view.getBottom() )); int[] position = new int[2]; transitionValues.view.getLocationInWindow(position); transitionValues.values.put(PROPERTY_POSITION, position); } @Override public void captureEndValues(TransitionValues transitionValues) { final View view = transitionValues.view; if (view.getWidth() <= 0 || view.getHeight() <= 0) { return; } captureValues(transitionValues); } @Override public void captureStartValues(TransitionValues transitionValues) { final View view = transitionValues.view; if (view.getWidth() <= 0 || view.getHeight() <= 0) { return; } captureValues(transitionValues); Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); view.draw(canvas); transitionValues.values.put(PROPERTY_IMAGE, bitmap); } @Override public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues, final TransitionValues endValues) { if (startValues == null || endValues == null) { return null; } Rect startBounds = (Rect) startValues.values.get(PROPERTY_BOUNDS); Rect endBounds = (Rect) endValues.values.get(PROPERTY_BOUNDS); if (startBounds == null || endBounds == null || startBounds.equals(endBounds)) { return null; } Bitmap startImage = (Bitmap) startValues.values.get(PROPERTY_IMAGE); Drawable startBackground = new BitmapDrawable(sceneRoot.getContext().getResources(), startImage); final View startView = addViewToOverlay(sceneRoot, startImage.getWidth(), startImage.getHeight(), startBackground); Drawable shrinkingBackground = new ColorDrawable(mColor); final View shrinkingView = addViewToOverlay(sceneRoot, startImage.getWidth(), startImage.getHeight(), shrinkingBackground); int[] sceneRootLoc = new int[2]; sceneRoot.getLocationInWindow(sceneRootLoc); int[] startLoc = (int[]) startValues.values.get(PROPERTY_POSITION); int startTranslationX = startLoc[0] - sceneRootLoc[0]; int startTranslationY = startLoc[1] - sceneRootLoc[1]; startView.setTranslationX(startTranslationX); startView.setTranslationY(startTranslationY); shrinkingView.setTranslationX(startTranslationX); shrinkingView.setTranslationY(startTranslationY); final View endView = endValues.view; float startRadius = calculateMaxRadius(shrinkingView); int minRadius = Math.min(calculateMinRadius(shrinkingView), calculateMinRadius(endView)); ShapeDrawable circleBackground = new ShapeDrawable(new OvalShape()); circleBackground.getPaint().setColor(mColor); final View circleView = addViewToOverlay(sceneRoot, minRadius * 2, minRadius * 2, circleBackground); float circleStartX = startLoc[0] - sceneRootLoc[0] + ((startView.getWidth() - circleView.getWidth()) / 2); float circleStartY = startLoc[1] - sceneRootLoc[1] + ((startView.getHeight() - circleView.getHeight()) / 2); circleView.setTranslationX(circleStartX); circleView.setTranslationY(circleStartY); circleView.setVisibility(View.INVISIBLE); shrinkingView.setAlpha(0f); endView.setAlpha(0f); Animator shrinkingAnimator = createCircularReveal(shrinkingView, startRadius, minRadius); shrinkingAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { shrinkingView.setVisibility(View.INVISIBLE); startView.setVisibility(View.INVISIBLE); circleView.setVisibility(View.VISIBLE); } }); Animator startAnimator = createCircularReveal(startView, startRadius, minRadius); Animator fadeInAnimator = ObjectAnimator.ofFloat(shrinkingView, View.ALPHA, 0, 1); AnimatorSet shrinkFadeSet = new AnimatorSet(); shrinkFadeSet.playTogether(shrinkingAnimator, startAnimator, fadeInAnimator); int[] endLoc = (int[]) endValues.values.get(PROPERTY_POSITION); float circleEndX = endLoc[0] - sceneRootLoc[0] + ((endView.getWidth() - circleView.getWidth()) / 2); float circleEndY = endLoc[1] - sceneRootLoc[1] + ((endView.getHeight() - circleView.getHeight()) / 2); Path circlePath = getPathMotion().getPath(circleStartX, circleStartY, circleEndX, circleEndY); Animator circleAnimator = ObjectAnimator.ofFloat(circleView, View.TRANSLATION_X, View.TRANSLATION_Y, circlePath); final View growingView = addViewToOverlay(sceneRoot, endView.getWidth(), endView.getHeight(), shrinkingBackground); growingView.setVisibility(View.INVISIBLE); float endTranslationX = endLoc[0] - sceneRootLoc[0]; float endTranslationY = endLoc[1] - sceneRootLoc[1]; growingView.setTranslationX(endTranslationX); growingView.setTranslationY(endTranslationY); float endRadius = calculateMaxRadius(endView); circleAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { circleView.setVisibility(View.INVISIBLE); growingView.setVisibility(View.VISIBLE); endView.setAlpha(1f); } }); Animator fadeOutAnimator = ObjectAnimator.ofFloat(growingView, View.ALPHA, 1, 0); Animator endAnimator = createCircularReveal(endView, minRadius, endRadius); Animator growingAnimator = createCircularReveal(growingView, minRadius, endRadius); growingAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { sceneRoot.getOverlay().remove(startView); sceneRoot.getOverlay().remove(shrinkingView); sceneRoot.getOverlay().remove(circleView); sceneRoot.getOverlay().remove(growingView); } }); AnimatorSet growingFadeSet = new AnimatorSet(); growingFadeSet.playTogether(fadeOutAnimator, endAnimator, growingAnimator); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.playSequentially(shrinkFadeSet, circleAnimator, growingFadeSet); return animatorSet; } private View addViewToOverlay(ViewGroup sceneRoot, int width, int height, Drawable background) { View view = new NoOverlapView(sceneRoot.getContext()); view.setBackground(background); int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); view.measure(widthSpec, heightSpec); view.layout(0, 0, width, height); sceneRoot.getOverlay().add(view); return view; } private Animator createCircularReveal(View view, float startRadius, float endRadius) { int centerX = view.getWidth() / 2; int centerY = view.getHeight() / 2; Animator reveal = ViewAnimationUtils.createCircularReveal(view, centerX, centerY, startRadius, endRadius); return new NoPauseAnimator(reveal); } static float calculateMaxRadius(View view) { float widthSquared = view.getWidth() * view.getWidth(); float heightSquared = view.getHeight() * view.getHeight(); float radius = (float) Math.sqrt(widthSquared + heightSquared) / 2; return radius; } static int calculateMinRadius(View view) { return Math.min(view.getWidth() / 2, view.getHeight() / 2); } private static class NoPauseAnimator extends Animator { private final Animator mAnimator; private final ArrayMap<AnimatorListener, AnimatorListener> mListeners = new ArrayMap<AnimatorListener, AnimatorListener>(); public NoPauseAnimator(Animator animator) { mAnimator = animator; } @Override public void addListener(AnimatorListener listener) { AnimatorListener wrapper = new AnimatorListenerWrapper(this, listener); if (!mListeners.containsKey(listener)) { mListeners.put(listener, wrapper); mAnimator.addListener(wrapper); } } @Override public void cancel() { mAnimator.cancel(); } @Override public void end() { mAnimator.end(); } @Override public long getDuration() { return mAnimator.getDuration(); } @Override public TimeInterpolator getInterpolator() { return mAnimator.getInterpolator(); } @Override public ArrayList<AnimatorListener> getListeners() { return new ArrayList<AnimatorListener>(mListeners.keySet()); } @Override public long getStartDelay() { return mAnimator.getStartDelay(); } @Override public boolean isPaused() { return mAnimator.isPaused(); } @Override public boolean isRunning() { return mAnimator.isRunning(); } @Override public boolean isStarted() { return mAnimator.isStarted(); } @Override public void removeAllListeners() { mListeners.clear(); mAnimator.removeAllListeners(); } @Override public void removeListener(AnimatorListener listener) { AnimatorListener wrapper = mListeners.get(listener); if (wrapper != null) { mListeners.remove(listener); mAnimator.removeListener(wrapper); } } @Override public Animator setDuration(long durationMS) { mAnimator.setDuration(durationMS); return this; } @Override public void setInterpolator(TimeInterpolator timeInterpolator) { mAnimator.setInterpolator(timeInterpolator); } @Override public void setStartDelay(long delayMS) { mAnimator.setStartDelay(delayMS); } @Override public void setTarget(Object target) { mAnimator.setTarget(target); } @Override public void setupEndValues() { mAnimator.setupEndValues(); } @Override public void setupStartValues() { mAnimator.setupStartValues(); } @Override public void start() { mAnimator.start(); } } private static class AnimatorListenerWrapper implements Animator.AnimatorListener { private final Animator mAnimator; private final Animator.AnimatorListener mListener; public AnimatorListenerWrapper(Animator animator, Animator.AnimatorListener listener) { mAnimator = animator; mListener = listener; } @Override public void onAnimationStart(Animator animator) { mListener.onAnimationStart(mAnimator); } @Override public void onAnimationEnd(Animator animator) { mListener.onAnimationEnd(mAnimator); } @Override public void onAnimationCancel(Animator animator) { mListener.onAnimationCancel(mAnimator); } @Override public void onAnimationRepeat(Animator animator) { mListener.onAnimationRepeat(mAnimator); } } private static class NoOverlapView extends View { public NoOverlapView(Context context) { super(context); } @Override public boolean hasOverlappingRendering() { return false; } } }