Home | History | Annotate | Download | only in incallui
      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.incallui;
     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.view.View;
     27 import android.view.ViewPropertyAnimator;
     28 import android.widget.ImageView;
     29 
     30 /**
     31  * Utilities for Animation.
     32  */
     33 public 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 }