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