Home | History | Annotate | Download | only in phone
      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 
     17 package com.android.phone;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.graphics.Canvas;
     23 import android.graphics.drawable.BitmapDrawable;
     24 import android.graphics.drawable.Drawable;
     25 import android.graphics.drawable.LayerDrawable;
     26 import android.util.Log;
     27 import android.view.View;
     28 import android.view.ViewPropertyAnimator;
     29 import android.widget.ImageView;
     30 
     31 /**
     32  * Utilities for Animation.
     33  */
     34 public 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 }