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}