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 AnimationUtils()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 */ show(final View view)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 */ hide(final View view, final int visibility)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 */ isFadingOut(final View view)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 CrossFadeDrawable(Drawable[] layers)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") setCrossFadeAlpha(int alpha)170 public void setCrossFadeAlpha(int alpha) { 171 mCrossFadeAlpha = alpha; 172 invalidateSelf(); 173 } 174 getAnimator()175 public ObjectAnimator getAnimator() { 176 return mAnimator; 177 } 178 179 @Override draw(Canvas canvas)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 newCrossFadeDrawable(Drawable first, Drawable second)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 */ startCrossFade( final ImageView imageView, final Drawable from, final Drawable to)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 log(String msg)276 private static void log(String msg) { 277 Log.d(LOG_TAG, msg); 278 } 279 }