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
17package android.transition;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.animation.RectEvaluator;
24import android.animation.ValueAnimator;
25import android.graphics.Bitmap;
26import android.graphics.Canvas;
27import android.graphics.Rect;
28import android.graphics.drawable.BitmapDrawable;
29import android.util.Log;
30import android.view.SurfaceView;
31import android.view.TextureView;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.ViewOverlay;
35
36import 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 */
47public 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}
297