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