/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.transition; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.util.Log; import android.view.SurfaceView; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import android.view.ViewOverlay; import java.util.Map; /** * This transition captures bitmap representations of target views before and * after the scene change and fades between them. * *

Note: This transition is not compatible with {@link TextureView} * or {@link SurfaceView}.

* * @hide */ public class Crossfade extends Transition { // TODO: Add a hook that lets a Transition call user code to query whether it should run on // a given target view. This would save bitmap comparisons in this transition, for example. private static final String LOG_TAG = "Crossfade"; private static final String PROPNAME_BITMAP = "android:crossfade:bitmap"; private static final String PROPNAME_DRAWABLE = "android:crossfade:drawable"; private static final String PROPNAME_BOUNDS = "android:crossfade:bounds"; private static RectEvaluator sRectEvaluator = new RectEvaluator(); private int mFadeBehavior = FADE_BEHAVIOR_REVEAL; private int mResizeBehavior = RESIZE_BEHAVIOR_SCALE; /** * Flag specifying that the fading animation should cross-fade * between the old and new representation of all affected target * views. This means that the old representation will fade out * while the new one fades in. This effect may work well on views * without solid backgrounds, such as TextViews. * * @see #setFadeBehavior(int) */ public static final int FADE_BEHAVIOR_CROSSFADE = 0; /** * Flag specifying that the fading animation should reveal the * new representation of all affected target views. This means * that the old representation will fade out, gradually * revealing the new representation, which remains opaque * the whole time. This effect may work well on views * with solid backgrounds, such as ImageViews. * * @see #setFadeBehavior(int) */ public static final int FADE_BEHAVIOR_REVEAL = 1; /** * Flag specifying that the fading animation should first fade * out the original representation completely and then fade in the * new one. This effect may be more suitable than the other * fade behaviors for views with. * * @see #setFadeBehavior(int) */ public static final int FADE_BEHAVIOR_OUT_IN = 2; /** * Flag specifying that the transition should not animate any * changes in size between the old and new target views. * This means that no scaling will take place as a result of * this transition * * @see #setResizeBehavior(int) */ public static final int RESIZE_BEHAVIOR_NONE = 0; /** * Flag specifying that the transition should animate any * changes in size between the old and new target views. * This means that the animation will scale the start/end * representations of affected views from the starting size * to the ending size over the course of the animation. * This effect may work well on images, but is not recommended * for text. * * @see #setResizeBehavior(int) */ public static final int RESIZE_BEHAVIOR_SCALE = 1; // TODO: Add fade/resize behaviors to xml resources /** * Sets the type of fading animation that will be run, one of * {@link #FADE_BEHAVIOR_CROSSFADE} and {@link #FADE_BEHAVIOR_REVEAL}. * * @param fadeBehavior The type of fading animation to use when this * transition is run. */ public Crossfade setFadeBehavior(int fadeBehavior) { if (fadeBehavior >= FADE_BEHAVIOR_CROSSFADE && fadeBehavior <= FADE_BEHAVIOR_OUT_IN) { mFadeBehavior = fadeBehavior; } return this; } /** * Returns the fading behavior of the animation. * * @return This crossfade object. * @see #setFadeBehavior(int) */ public int getFadeBehavior() { return mFadeBehavior; } /** * Sets the type of resizing behavior that will be used during the * transition animation, one of {@link #RESIZE_BEHAVIOR_NONE} and * {@link #RESIZE_BEHAVIOR_SCALE}. * * @param resizeBehavior The type of resizing behavior to use when this * transition is run. */ public Crossfade setResizeBehavior(int resizeBehavior) { if (resizeBehavior >= RESIZE_BEHAVIOR_NONE && resizeBehavior <= RESIZE_BEHAVIOR_SCALE) { mResizeBehavior = resizeBehavior; } return this; } /** * Returns the resizing behavior of the animation. * * @return This crossfade object. * @see #setResizeBehavior(int) */ public int getResizeBehavior() { return mResizeBehavior; } @Override public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { if (startValues == null || endValues == null) { return null; } final boolean useParentOverlay = mFadeBehavior != FADE_BEHAVIOR_REVEAL; final View view = endValues.view; Map startVals = startValues.values; Map endVals = endValues.values; Rect startBounds = (Rect) startVals.get(PROPNAME_BOUNDS); Rect endBounds = (Rect) endVals.get(PROPNAME_BOUNDS); Bitmap startBitmap = (Bitmap) startVals.get(PROPNAME_BITMAP); Bitmap endBitmap = (Bitmap) endVals.get(PROPNAME_BITMAP); final BitmapDrawable startDrawable = (BitmapDrawable) startVals.get(PROPNAME_DRAWABLE); final BitmapDrawable endDrawable = (BitmapDrawable) endVals.get(PROPNAME_DRAWABLE); if (Transition.DBG) { Log.d(LOG_TAG, "StartBitmap.sameAs(endBitmap) = " + startBitmap.sameAs(endBitmap) + " for start, end: " + startBitmap + ", " + endBitmap); } if (startDrawable != null && endDrawable != null && !startBitmap.sameAs(endBitmap)) { ViewOverlay overlay = useParentOverlay ? ((ViewGroup) view.getParent()).getOverlay() : view.getOverlay(); if (mFadeBehavior == FADE_BEHAVIOR_REVEAL) { overlay.add(endDrawable); } overlay.add(startDrawable); // The transition works by placing the end drawable under the start drawable and // gradually fading out the start drawable. So it's not really a cross-fade, but rather // a reveal of the end scene over time. Also, animate the bounds of both drawables // to mimic the change in the size of the view itself between scenes. ObjectAnimator anim; if (mFadeBehavior == FADE_BEHAVIOR_OUT_IN) { // Fade out completely halfway through the transition anim = ObjectAnimator.ofInt(startDrawable, "alpha", 255, 0, 0); } else { anim = ObjectAnimator.ofInt(startDrawable, "alpha", 0); } anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { // TODO: some way to auto-invalidate views based on drawable changes? callbacks? view.invalidate(startDrawable.getBounds()); } }); ObjectAnimator anim1 = null; if (mFadeBehavior == FADE_BEHAVIOR_OUT_IN) { // start fading in halfway through the transition anim1 = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 0, 1); } else if (mFadeBehavior == FADE_BEHAVIOR_CROSSFADE) { anim1 = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1); } if (Transition.DBG) { Log.d(LOG_TAG, "Crossfade: created anim " + anim + " for start, end values " + startValues + ", " + endValues); } anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { ViewOverlay overlay = useParentOverlay ? ((ViewGroup) view.getParent()).getOverlay() : view.getOverlay(); overlay.remove(startDrawable); if (mFadeBehavior == FADE_BEHAVIOR_REVEAL) { overlay.remove(endDrawable); } } }); AnimatorSet set = new AnimatorSet(); set.playTogether(anim); if (anim1 != null) { set.playTogether(anim1); } if (mResizeBehavior == RESIZE_BEHAVIOR_SCALE && !startBounds.equals(endBounds)) { if (Transition.DBG) { Log.d(LOG_TAG, "animating from startBounds to endBounds: " + startBounds + ", " + endBounds); } Animator anim2 = ObjectAnimator.ofObject(startDrawable, "bounds", sRectEvaluator, startBounds, endBounds); set.playTogether(anim2); if (mResizeBehavior == RESIZE_BEHAVIOR_SCALE) { // TODO: How to handle resizing with a CROSSFADE (vs. REVEAL) effect // when we are animating the view directly? Animator anim3 = ObjectAnimator.ofObject(endDrawable, "bounds", sRectEvaluator, startBounds, endBounds); set.playTogether(anim3); } } return set; } else { return null; } } private void captureValues(TransitionValues transitionValues) { View view = transitionValues.view; Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight()); if (mFadeBehavior != FADE_BEHAVIOR_REVEAL) { bounds.offset(view.getLeft(), view.getTop()); } transitionValues.values.put(PROPNAME_BOUNDS, bounds); if (Transition.DBG) { Log.d(LOG_TAG, "Captured bounds " + transitionValues.values.get(PROPNAME_BOUNDS)); } Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); if (view instanceof TextureView) { bitmap = ((TextureView) view).getBitmap(); } else { Canvas c = new Canvas(bitmap); view.draw(c); } transitionValues.values.put(PROPNAME_BITMAP, bitmap); // TODO: I don't have resources, can't call the non-deprecated method? BitmapDrawable drawable = new BitmapDrawable(bitmap); // TODO: lrtb will be wrong if the view has transXY set drawable.setBounds(bounds); transitionValues.values.put(PROPNAME_DRAWABLE, drawable); } @Override public void captureStartValues(TransitionValues transitionValues) { captureValues(transitionValues); } @Override public void captureEndValues(TransitionValues transitionValues) { captureValues(transitionValues); } }