1/*
2 * Copyright (C) 2012 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 com.android.phone;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.graphics.Canvas;
23import android.graphics.drawable.BitmapDrawable;
24import android.graphics.drawable.Drawable;
25import android.graphics.drawable.LayerDrawable;
26import android.util.Log;
27import android.view.View;
28import android.view.ViewPropertyAnimator;
29import android.widget.ImageView;
30
31/**
32 * Utilities for Animation.
33 */
34public class AnimationUtils {
35    private static final String LOG_TAG = AnimationUtils.class.getSimpleName();
36    /**
37     * Turn on when you're interested in fading animation. Intentionally untied from other debug
38     * settings.
39     */
40    private static final boolean FADE_DBG = false;
41
42    /**
43     * Duration for animations in msec, which can be used with
44     * {@link ViewPropertyAnimator#setDuration(long)} for example.
45     */
46    public static final int ANIMATION_DURATION = 250;
47
48    private AnimationUtils() {
49    }
50
51    /**
52     * Simple Utility class that runs fading animations on specified views.
53     */
54    public static class Fade {
55
56        // View tag that's set during the fade-out animation; see hide() and
57        // isFadingOut().
58        private static final int FADE_STATE_KEY = R.id.fadeState;
59        private static final String FADING_OUT = "fading_out";
60
61        /**
62         * Sets the visibility of the specified view to View.VISIBLE and then
63         * fades it in. If the view is already visible (and not in the middle
64         * of a fade-out animation), this method will return without doing
65         * anything.
66         *
67         * @param view The view to be faded in
68         */
69        public static void show(final View view) {
70            if (FADE_DBG) log("Fade: SHOW view " + view + "...");
71            if (FADE_DBG) log("Fade: - visibility = " + view.getVisibility());
72            if ((view.getVisibility() != View.VISIBLE) || isFadingOut(view)) {
73                view.animate().cancel();
74                // ...and clear the FADE_STATE_KEY tag in case we just
75                // canceled an in-progress fade-out animation.
76                view.setTag(FADE_STATE_KEY, null);
77
78                view.setAlpha(0);
79                view.setVisibility(View.VISIBLE);
80                view.animate().setDuration(ANIMATION_DURATION);
81                view.animate().alpha(1);
82                if (FADE_DBG) log("Fade: ==> SHOW " + view
83                                  + " DONE.  Set visibility = " + View.VISIBLE);
84            } else {
85                if (FADE_DBG) log("Fade: ==> Ignoring, already visible AND not fading out.");
86            }
87        }
88
89        /**
90         * Fades out the specified view and then sets its visibility to the
91         * specified value (either View.INVISIBLE or View.GONE). If the view
92         * is not currently visibile, the method will return without doing
93         * anything.
94         *
95         * Note that *during* the fade-out the view itself will still have
96         * visibility View.VISIBLE, although the isFadingOut() method will
97         * return true (in case the UI code needs to detect this state.)
98         *
99         * @param view The view to be hidden
100         * @param visibility The value to which the view's visibility will be
101         *                   set after it fades out.
102         *                   Must be either View.INVISIBLE or View.GONE.
103         */
104        public static void hide(final View view, final int visibility) {
105            if (FADE_DBG) log("Fade: HIDE view " + view + "...");
106            if (view.getVisibility() == View.VISIBLE &&
107                (visibility == View.INVISIBLE || visibility == View.GONE)) {
108
109                // Use a view tag to mark this view as being in the middle
110                // of a fade-out animation.
111                view.setTag(FADE_STATE_KEY, FADING_OUT);
112
113                view.animate().cancel();
114                view.animate().setDuration(ANIMATION_DURATION);
115                view.animate().alpha(0f).setListener(new AnimatorListenerAdapter() {
116                    @Override
117                    public void onAnimationEnd(Animator animation) {
118                        view.setAlpha(1);
119                        view.setVisibility(visibility);
120                        view.animate().setListener(null);
121                        // ...and we're done with the fade-out, so clear the view tag.
122                        view.setTag(FADE_STATE_KEY, null);
123                        if (FADE_DBG) log("Fade: HIDE " + view
124                                + " DONE.  Set visibility = " + visibility);
125                    }
126                });
127            }
128        }
129
130        /**
131         * @return true if the specified view is currently in the middle
132         * of a fade-out animation.  (During the fade-out, the view's
133         * visibility is still VISIBLE, although in many cases the UI
134         * should behave as if it's already invisible or gone.  This
135         * method allows the UI code to detect that state.)
136         *
137         * @see #hide(View, int)
138         */
139        public static boolean isFadingOut(final View view) {
140            if (FADE_DBG) {
141                log("Fade: isFadingOut view " + view + "...");
142                log("Fade:   - getTag() returns: " + view.getTag(FADE_STATE_KEY));
143                log("Fade:   - returning: " + (view.getTag(FADE_STATE_KEY) == FADING_OUT));
144            }
145            return (view.getTag(FADE_STATE_KEY) == FADING_OUT);
146        }
147
148    }
149
150    /**
151     * Drawable achieving cross-fade, just like TransitionDrawable. We can have
152     * call-backs via animator object (see also {@link CrossFadeDrawable#getAnimator()}).
153     */
154    private static class CrossFadeDrawable extends LayerDrawable {
155        private final ObjectAnimator mAnimator;
156
157        public CrossFadeDrawable(Drawable[] layers) {
158            super(layers);
159            mAnimator = ObjectAnimator.ofInt(this, "crossFadeAlpha", 0xff, 0);
160        }
161
162        private int mCrossFadeAlpha;
163
164        /**
165         * This will be used from ObjectAnimator.
166         * Note: this method is protected by proguard.flags so that it won't be removed
167         * automatically.
168         */
169        @SuppressWarnings("unused")
170        public void setCrossFadeAlpha(int alpha) {
171            mCrossFadeAlpha = alpha;
172            invalidateSelf();
173        }
174
175        public ObjectAnimator getAnimator() {
176            return mAnimator;
177        }
178
179        @Override
180        public void draw(Canvas canvas) {
181            Drawable first = getDrawable(0);
182            Drawable second = getDrawable(1);
183
184            if (mCrossFadeAlpha > 0) {
185                first.setAlpha(mCrossFadeAlpha);
186                first.draw(canvas);
187                first.setAlpha(255);
188            }
189
190            if (mCrossFadeAlpha < 0xff) {
191                second.setAlpha(0xff - mCrossFadeAlpha);
192                second.draw(canvas);
193                second.setAlpha(0xff);
194            }
195        }
196    }
197
198    private static CrossFadeDrawable newCrossFadeDrawable(Drawable first, Drawable second) {
199        Drawable[] layers = new Drawable[2];
200        layers[0] = first;
201        layers[1] = second;
202        return new CrossFadeDrawable(layers);
203    }
204
205    /**
206     * Starts cross-fade animation using TransitionDrawable. Nothing will happen if "from" and "to"
207     * are the same.
208     */
209    public static void startCrossFade(
210            final ImageView imageView, final Drawable from, final Drawable to) {
211        // We skip the cross-fade when those two Drawables are equal, or they are BitmapDrawables
212        // pointing to the same Bitmap.
213        final boolean areSameImage = from.equals(to) ||
214                ((from instanceof BitmapDrawable)
215                        && (to instanceof BitmapDrawable)
216                        && ((BitmapDrawable) from).getBitmap()
217                                .equals(((BitmapDrawable) to).getBitmap()));
218        if (!areSameImage) {
219            if (FADE_DBG) {
220                log("Start cross-fade animation for " + imageView
221                        + "(" + Integer.toHexString(from.hashCode()) + " -> "
222                        + Integer.toHexString(to.hashCode()) + ")");
223            }
224
225            CrossFadeDrawable crossFadeDrawable = newCrossFadeDrawable(from, to);
226            ObjectAnimator animator = crossFadeDrawable.getAnimator();
227            imageView.setImageDrawable(crossFadeDrawable);
228            animator.setDuration(ANIMATION_DURATION);
229            animator.addListener(new AnimatorListenerAdapter() {
230                @Override
231                public void onAnimationStart(Animator animation) {
232                    if (FADE_DBG) {
233                        log("cross-fade animation start ("
234                                + Integer.toHexString(from.hashCode()) + " -> "
235                                + Integer.toHexString(to.hashCode()) + ")");
236                    }
237                }
238
239                @Override
240                public void onAnimationEnd(Animator animation) {
241                    if (FADE_DBG) {
242                        log("cross-fade animation ended ("
243                                + Integer.toHexString(from.hashCode()) + " -> "
244                                + Integer.toHexString(to.hashCode()) + ")");
245                    }
246                    animation.removeAllListeners();
247                    // Workaround for issue 6300562; this will force the drawable to the
248                    // resultant one regardless of animation glitch.
249                    imageView.setImageDrawable(to);
250                }
251            });
252            animator.start();
253
254            /* We could use TransitionDrawable here, but it may cause some weird animation in
255             * some corner cases. See issue 6300562
256             * TODO: decide which to be used in the long run. TransitionDrawable is old but system
257             * one. Ours uses new animation framework and thus have callback (great for testing),
258             * while no framework support for the exact class.
259
260            Drawable[] layers = new Drawable[2];
261            layers[0] = from;
262            layers[1] = to;
263            TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
264            imageView.setImageDrawable(transitionDrawable);
265            transitionDrawable.startTransition(ANIMATION_DURATION); */
266            imageView.setTag(to);
267        } else {
268            if (FADE_DBG) {
269                log("*Not* start cross-fade. " + imageView);
270            }
271        }
272    }
273
274    // Debugging / testing code
275
276    private static void log(String msg) {
277        Log.d(LOG_TAG, msg);
278    }
279}