1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.transition; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.animation.RectEvaluator; 24 import android.animation.ValueAnimator; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.graphics.Bitmap; 28 import android.graphics.Canvas; 29 import android.graphics.Rect; 30 import android.graphics.drawable.BitmapDrawable; 31 import android.util.Log; 32 import android.view.SurfaceView; 33 import android.view.TextureView; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.ViewOverlay; 37 38 import java.util.Map; 39 40 /** 41 * This transition captures bitmap representations of target views before and 42 * after the scene change and fades between them. 43 * 44 * <p>Note: This transition is not compatible with {@link TextureView} 45 * or {@link SurfaceView}.</p> 46 * 47 * @hide 48 */ 49 public class Crossfade extends Transition { 50 // TODO: Add a hook that lets a Transition call user code to query whether it should run on 51 // a given target view. This would save bitmap comparisons in this transition, for example. 52 53 private static final String LOG_TAG = "Crossfade"; 54 55 private static final String PROPNAME_BITMAP = "android:crossfade:bitmap"; 56 private static final String PROPNAME_DRAWABLE = "android:crossfade:drawable"; 57 private static final String PROPNAME_BOUNDS = "android:crossfade:bounds"; 58 59 private static RectEvaluator sRectEvaluator = new RectEvaluator(); 60 61 private int mFadeBehavior = FADE_BEHAVIOR_REVEAL; 62 private int mResizeBehavior = RESIZE_BEHAVIOR_SCALE; 63 64 /** 65 * Flag specifying that the fading animation should cross-fade 66 * between the old and new representation of all affected target 67 * views. This means that the old representation will fade out 68 * while the new one fades in. This effect may work well on views 69 * without solid backgrounds, such as TextViews. 70 * 71 * @see #setFadeBehavior(int) 72 */ 73 public static final int FADE_BEHAVIOR_CROSSFADE = 0; 74 /** 75 * Flag specifying that the fading animation should reveal the 76 * new representation of all affected target views. This means 77 * that the old representation will fade out, gradually 78 * revealing the new representation, which remains opaque 79 * the whole time. This effect may work well on views 80 * with solid backgrounds, such as ImageViews. 81 * 82 * @see #setFadeBehavior(int) 83 */ 84 public static final int FADE_BEHAVIOR_REVEAL = 1; 85 /** 86 * Flag specifying that the fading animation should first fade 87 * out the original representation completely and then fade in the 88 * new one. This effect may be more suitable than the other 89 * fade behaviors for views with. 90 * 91 * @see #setFadeBehavior(int) 92 */ 93 public static final int FADE_BEHAVIOR_OUT_IN = 2; 94 95 /** 96 * Flag specifying that the transition should not animate any 97 * changes in size between the old and new target views. 98 * This means that no scaling will take place as a result of 99 * this transition 100 * 101 * @see #setResizeBehavior(int) 102 */ 103 public static final int RESIZE_BEHAVIOR_NONE = 0; 104 /** 105 * Flag specifying that the transition should animate any 106 * changes in size between the old and new target views. 107 * This means that the animation will scale the start/end 108 * representations of affected views from the starting size 109 * to the ending size over the course of the animation. 110 * This effect may work well on images, but is not recommended 111 * for text. 112 * 113 * @see #setResizeBehavior(int) 114 */ 115 public static final int RESIZE_BEHAVIOR_SCALE = 1; 116 117 // TODO: Add fade/resize behaviors to xml resources 118 119 /** 120 * Sets the type of fading animation that will be run, one of 121 * {@link #FADE_BEHAVIOR_CROSSFADE} and {@link #FADE_BEHAVIOR_REVEAL}. 122 * 123 * @param fadeBehavior The type of fading animation to use when this 124 * transition is run. 125 */ setFadeBehavior(int fadeBehavior)126 public Crossfade setFadeBehavior(int fadeBehavior) { 127 if (fadeBehavior >= FADE_BEHAVIOR_CROSSFADE && fadeBehavior <= FADE_BEHAVIOR_OUT_IN) { 128 mFadeBehavior = fadeBehavior; 129 } 130 return this; 131 } 132 133 /** 134 * Returns the fading behavior of the animation. 135 * 136 * @return This crossfade object. 137 * @see #setFadeBehavior(int) 138 */ getFadeBehavior()139 public int getFadeBehavior() { 140 return mFadeBehavior; 141 } 142 143 /** 144 * Sets the type of resizing behavior that will be used during the 145 * transition animation, one of {@link #RESIZE_BEHAVIOR_NONE} and 146 * {@link #RESIZE_BEHAVIOR_SCALE}. 147 * 148 * @param resizeBehavior The type of resizing behavior to use when this 149 * transition is run. 150 */ setResizeBehavior(int resizeBehavior)151 public Crossfade setResizeBehavior(int resizeBehavior) { 152 if (resizeBehavior >= RESIZE_BEHAVIOR_NONE && resizeBehavior <= RESIZE_BEHAVIOR_SCALE) { 153 mResizeBehavior = resizeBehavior; 154 } 155 return this; 156 } 157 158 /** 159 * Returns the resizing behavior of the animation. 160 * 161 * @return This crossfade object. 162 * @see #setResizeBehavior(int) 163 */ getResizeBehavior()164 public int getResizeBehavior() { 165 return mResizeBehavior; 166 } 167 168 @Nullable 169 @Override createAnimator(@onNull ViewGroup sceneRoot, @Nullable TransitionValues startValues, @Nullable TransitionValues endValues)170 public Animator createAnimator(@NonNull ViewGroup sceneRoot, 171 @Nullable TransitionValues startValues, 172 @Nullable TransitionValues endValues) { 173 if (startValues == null || endValues == null) { 174 return null; 175 } 176 final boolean useParentOverlay = mFadeBehavior != FADE_BEHAVIOR_REVEAL; 177 final View view = endValues.view; 178 Map<String, Object> startVals = startValues.values; 179 Map<String, Object> endVals = endValues.values; 180 Rect startBounds = (Rect) startVals.get(PROPNAME_BOUNDS); 181 Rect endBounds = (Rect) endVals.get(PROPNAME_BOUNDS); 182 Bitmap startBitmap = (Bitmap) startVals.get(PROPNAME_BITMAP); 183 Bitmap endBitmap = (Bitmap) endVals.get(PROPNAME_BITMAP); 184 final BitmapDrawable startDrawable = (BitmapDrawable) startVals.get(PROPNAME_DRAWABLE); 185 final BitmapDrawable endDrawable = (BitmapDrawable) endVals.get(PROPNAME_DRAWABLE); 186 if (Transition.DBG) { 187 Log.d(LOG_TAG, "StartBitmap.sameAs(endBitmap) = " + startBitmap.sameAs(endBitmap) + 188 " for start, end: " + startBitmap + ", " + endBitmap); 189 } 190 if (startDrawable != null && endDrawable != null && !startBitmap.sameAs(endBitmap)) { 191 ViewOverlay overlay = useParentOverlay ? 192 ((ViewGroup) view.getParent()).getOverlay() : view.getOverlay(); 193 if (mFadeBehavior == FADE_BEHAVIOR_REVEAL) { 194 overlay.add(endDrawable); 195 } 196 overlay.add(startDrawable); 197 // The transition works by placing the end drawable under the start drawable and 198 // gradually fading out the start drawable. So it's not really a cross-fade, but rather 199 // a reveal of the end scene over time. Also, animate the bounds of both drawables 200 // to mimic the change in the size of the view itself between scenes. 201 ObjectAnimator anim; 202 if (mFadeBehavior == FADE_BEHAVIOR_OUT_IN) { 203 // Fade out completely halfway through the transition 204 anim = ObjectAnimator.ofInt(startDrawable, "alpha", 255, 0, 0); 205 } else { 206 anim = ObjectAnimator.ofInt(startDrawable, "alpha", 0); 207 } 208 anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 209 @Override 210 public void onAnimationUpdate(ValueAnimator animation) { 211 // TODO: some way to auto-invalidate views based on drawable changes? callbacks? 212 view.invalidate(startDrawable.getBounds()); 213 } 214 }); 215 ObjectAnimator anim1 = null; 216 if (mFadeBehavior == FADE_BEHAVIOR_OUT_IN) { 217 // start fading in halfway through the transition 218 anim1 = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 0, 1); 219 } else if (mFadeBehavior == FADE_BEHAVIOR_CROSSFADE) { 220 anim1 = ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1); 221 } 222 if (Transition.DBG) { 223 Log.d(LOG_TAG, "Crossfade: created anim " + anim + " for start, end values " + 224 startValues + ", " + endValues); 225 } 226 anim.addListener(new AnimatorListenerAdapter() { 227 @Override 228 public void onAnimationEnd(Animator animation) { 229 ViewOverlay overlay = useParentOverlay ? 230 ((ViewGroup) view.getParent()).getOverlay() : view.getOverlay(); 231 overlay.remove(startDrawable); 232 if (mFadeBehavior == FADE_BEHAVIOR_REVEAL) { 233 overlay.remove(endDrawable); 234 } 235 } 236 }); 237 AnimatorSet set = new AnimatorSet(); 238 set.playTogether(anim); 239 if (anim1 != null) { 240 set.playTogether(anim1); 241 } 242 if (mResizeBehavior == RESIZE_BEHAVIOR_SCALE && !startBounds.equals(endBounds)) { 243 if (Transition.DBG) { 244 Log.d(LOG_TAG, "animating from startBounds to endBounds: " + 245 startBounds + ", " + endBounds); 246 } 247 Animator anim2 = ObjectAnimator.ofObject(startDrawable, "bounds", 248 sRectEvaluator, startBounds, endBounds); 249 set.playTogether(anim2); 250 if (mResizeBehavior == RESIZE_BEHAVIOR_SCALE) { 251 // TODO: How to handle resizing with a CROSSFADE (vs. REVEAL) effect 252 // when we are animating the view directly? 253 Animator anim3 = ObjectAnimator.ofObject(endDrawable, "bounds", 254 sRectEvaluator, startBounds, endBounds); 255 set.playTogether(anim3); 256 } 257 } 258 return set; 259 } else { 260 return null; 261 } 262 } 263 captureValues(TransitionValues transitionValues)264 private void captureValues(TransitionValues transitionValues) { 265 View view = transitionValues.view; 266 Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight()); 267 if (mFadeBehavior != FADE_BEHAVIOR_REVEAL) { 268 bounds.offset(view.getLeft(), view.getTop()); 269 } 270 transitionValues.values.put(PROPNAME_BOUNDS, bounds); 271 272 if (Transition.DBG) { 273 Log.d(LOG_TAG, "Captured bounds " + transitionValues.values.get(PROPNAME_BOUNDS)); 274 } 275 Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), 276 Bitmap.Config.ARGB_8888); 277 if (view instanceof TextureView) { 278 bitmap = ((TextureView) view).getBitmap(); 279 } else { 280 Canvas c = new Canvas(bitmap); 281 view.draw(c); 282 } 283 transitionValues.values.put(PROPNAME_BITMAP, bitmap); 284 // TODO: I don't have resources, can't call the non-deprecated method? 285 BitmapDrawable drawable = new BitmapDrawable(bitmap); 286 // TODO: lrtb will be wrong if the view has transXY set 287 drawable.setBounds(bounds); 288 transitionValues.values.put(PROPNAME_DRAWABLE, drawable); 289 } 290 291 @Override captureStartValues(TransitionValues transitionValues)292 public void captureStartValues(TransitionValues transitionValues) { 293 captureValues(transitionValues); 294 } 295 296 @Override captureEndValues(TransitionValues transitionValues)297 public void captureEndValues(TransitionValues transitionValues) { 298 captureValues(transitionValues); 299 } 300 } 301