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      */
setFadeBehavior(int fadeBehavior)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      */
getFadeBehavior()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      */
setResizeBehavior(int resizeBehavior)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      */
getResizeBehavior()162     public int getResizeBehavior() {
163         return mResizeBehavior;
164     }
165 
166     @Override
createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues)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 
captureValues(TransitionValues transitionValues)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
captureStartValues(TransitionValues transitionValues)288     public void captureStartValues(TransitionValues transitionValues) {
289         captureValues(transitionValues);
290     }
291 
292     @Override
captureEndValues(TransitionValues transitionValues)293     public void captureEndValues(TransitionValues transitionValues) {
294         captureValues(transitionValues);
295     }
296 }
297