1 /* 2 * Copyright (C) 2014 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.deskclock; 18 19 import android.animation.Animator; 20 import android.animation.ArgbEvaluator; 21 import android.animation.ObjectAnimator; 22 import android.animation.PropertyValuesHolder; 23 import android.animation.TypeEvaluator; 24 import android.animation.ValueAnimator; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Animatable; 27 import android.graphics.drawable.Drawable; 28 import android.graphics.drawable.LayerDrawable; 29 import android.support.v4.graphics.drawable.DrawableCompat; 30 import android.support.v4.view.animation.FastOutSlowInInterpolator; 31 import android.util.Property; 32 import android.view.View; 33 import android.view.animation.Interpolator; 34 import android.widget.ImageView; 35 36 import java.lang.reflect.InvocationTargetException; 37 import java.lang.reflect.Method; 38 39 public class AnimatorUtils { 40 41 public static final Interpolator DECELERATE_ACCELERATE_INTERPOLATOR = new Interpolator() { 42 @Override 43 public float getInterpolation(float x) { 44 return 0.5f + 4.0f * (x - 0.5f) * (x - 0.5f) * (x - 0.5f); 45 } 46 }; 47 48 public static final Interpolator INTERPOLATOR_FAST_OUT_SLOW_IN = 49 new FastOutSlowInInterpolator(); 50 51 public static final Property<View, Integer> BACKGROUND_ALPHA = 52 new Property<View, Integer>(Integer.class, "background.alpha") { 53 @Override 54 public Integer get(View view) { 55 Drawable background = view.getBackground(); 56 if (background instanceof LayerDrawable 57 && ((LayerDrawable) background).getNumberOfLayers() > 0) { 58 background = ((LayerDrawable) background).getDrawable(0); 59 } 60 return background.getAlpha(); 61 } 62 63 @Override 64 public void set(View view, Integer value) { 65 setBackgroundAlpha(view, value); 66 } 67 }; 68 69 /** 70 * Sets the alpha of the top layer's drawable (of the background) only, if the background is a 71 * layer drawable, to ensure that the other layers (i.e., the selectable item background, and 72 * therefore the touch feedback RippleDrawable) are not affected. 73 * 74 * @param view the affected view 75 * @param value the alpha value (0-255) 76 */ 77 public static void setBackgroundAlpha(View view, Integer value) { 78 Drawable background = view.getBackground(); 79 if (background instanceof LayerDrawable 80 && ((LayerDrawable) background).getNumberOfLayers() > 0) { 81 background = ((LayerDrawable) background).getDrawable(0); 82 } 83 background.setAlpha(value); 84 } 85 86 public static final Property<ImageView, Integer> DRAWABLE_ALPHA = 87 new Property<ImageView, Integer>(Integer.class, "drawable.alpha") { 88 @Override 89 public Integer get(ImageView view) { 90 return view.getDrawable().getAlpha(); 91 } 92 93 @Override 94 public void set(ImageView view, Integer value) { 95 view.getDrawable().setAlpha(value); 96 } 97 }; 98 99 public static final Property<ImageView, Integer> DRAWABLE_TINT = 100 new Property<ImageView, Integer>(Integer.class, "drawable.tint") { 101 @Override 102 public Integer get(ImageView view) { 103 return null; 104 } 105 106 @Override 107 public void set(ImageView view, Integer value) { 108 // Ensure the drawable is wrapped using DrawableCompat. 109 final Drawable drawable = view.getDrawable(); 110 final Drawable wrappedDrawable = DrawableCompat.wrap(drawable); 111 if (wrappedDrawable != drawable) { 112 view.setImageDrawable(wrappedDrawable); 113 } 114 // Set the new tint value via DrawableCompat. 115 DrawableCompat.setTint(wrappedDrawable, value); 116 } 117 }; 118 119 @SuppressWarnings("unchecked") 120 public static final TypeEvaluator<Integer> ARGB_EVALUATOR = new ArgbEvaluator(); 121 122 private static Method sAnimateValue; 123 private static boolean sTryAnimateValue = true; 124 125 public static void setAnimatedFraction(ValueAnimator animator, float fraction) { 126 if (Utils.isLMR1OrLater()) { 127 animator.setCurrentFraction(fraction); 128 return; 129 } 130 131 if (sTryAnimateValue) { 132 // try to set the animated fraction directly so that it isn't affected by the 133 // internal animator scale or time (b/17938711) 134 try { 135 if (sAnimateValue == null) { 136 sAnimateValue = ValueAnimator.class 137 .getDeclaredMethod("animateValue", float.class); 138 sAnimateValue.setAccessible(true); 139 } 140 141 sAnimateValue.invoke(animator, fraction); 142 return; 143 } catch (NoSuchMethodException | InvocationTargetException 144 | IllegalAccessException e) { 145 // something went wrong, don't try that again 146 LogUtils.e("Unable to use animateValue directly", e); 147 sTryAnimateValue = false; 148 } 149 } 150 151 // if that doesn't work then just fall back to setting the current play time 152 animator.setCurrentPlayTime(Math.round(fraction * animator.getDuration())); 153 } 154 155 public static void reverse(ValueAnimator... animators) { 156 for (ValueAnimator animator : animators) { 157 final float fraction = animator.getAnimatedFraction(); 158 if (fraction > 0.0f) { 159 animator.reverse(); 160 setAnimatedFraction(animator, 1.0f - fraction); 161 } 162 } 163 } 164 165 public static void cancel(ValueAnimator... animators) { 166 for (ValueAnimator animator : animators) { 167 animator.cancel(); 168 } 169 } 170 171 public static ValueAnimator getScaleAnimator(View view, float... values) { 172 return ObjectAnimator.ofPropertyValuesHolder(view, 173 PropertyValuesHolder.ofFloat(View.SCALE_X, values), 174 PropertyValuesHolder.ofFloat(View.SCALE_Y, values)); 175 } 176 177 public static ValueAnimator getAlphaAnimator(View view, float... values) { 178 return ObjectAnimator.ofFloat(view, View.ALPHA, values); 179 } 180 181 public static final Property<View, Integer> VIEW_LEFT = 182 new Property<View, Integer>(Integer.class, "left") { 183 @Override 184 public Integer get(View view) { 185 return view.getLeft(); 186 } 187 188 @Override 189 public void set(View view, Integer left) { 190 view.setLeft(left); 191 } 192 }; 193 194 public static final Property<View, Integer> VIEW_TOP = 195 new Property<View, Integer>(Integer.class, "top") { 196 @Override 197 public Integer get(View view) { 198 return view.getTop(); 199 } 200 201 @Override 202 public void set(View view, Integer top) { 203 view.setTop(top); 204 } 205 }; 206 207 public static final Property<View, Integer> VIEW_BOTTOM = 208 new Property<View, Integer>(Integer.class, "bottom") { 209 @Override 210 public Integer get(View view) { 211 return view.getBottom(); 212 } 213 214 @Override 215 public void set(View view, Integer bottom) { 216 view.setBottom(bottom); 217 } 218 }; 219 220 public static final Property<View, Integer> VIEW_RIGHT = 221 new Property<View, Integer>(Integer.class, "right") { 222 @Override 223 public Integer get(View view) { 224 return view.getRight(); 225 } 226 227 @Override 228 public void set(View view, Integer right) { 229 view.setRight(right); 230 } 231 }; 232 233 /** 234 * @param target the view to be morphed 235 * @param from the bounds of the {@code target} before animating 236 * @param to the bounds of the {@code target} after animating 237 * @return an animator that morphs the {@code target} between the {@code from} bounds and the 238 * {@code to} bounds. Note that it is the *content* bounds that matter here, so padding 239 * insets contributed by the background are subtracted from the views when computing the 240 * {@code target} bounds. 241 */ 242 public static Animator getBoundsAnimator(View target, View from, View to) { 243 // Fetch the content insets for the views. Content bounds are what matter, not total bounds. 244 final Rect targetInsets = new Rect(); 245 target.getBackground().getPadding(targetInsets); 246 final Rect fromInsets = new Rect(); 247 from.getBackground().getPadding(fromInsets); 248 final Rect toInsets = new Rect(); 249 to.getBackground().getPadding(toInsets); 250 251 // Before animating, the content bounds of target must match the content bounds of from. 252 final int startLeft = from.getLeft() - fromInsets.left + targetInsets.left; 253 final int startTop = from.getTop() - fromInsets.top + targetInsets.top; 254 final int startRight = from.getRight() - fromInsets.right + targetInsets.right; 255 final int startBottom = from.getBottom() - fromInsets.bottom + targetInsets.bottom; 256 257 // After animating, the content bounds of target must match the content bounds of to. 258 final int endLeft = to.getLeft() - toInsets.left + targetInsets.left; 259 final int endTop = to.getTop() - toInsets.top + targetInsets.top; 260 final int endRight = to.getRight() - toInsets.right + targetInsets.right; 261 final int endBottom = to.getBottom() - toInsets.bottom + targetInsets.bottom; 262 263 return getBoundsAnimator(target, startLeft, startTop, startRight, startBottom, endLeft, 264 endTop, endRight, endBottom); 265 } 266 267 /** 268 * Returns an animator that animates the bounds of a single view. 269 */ 270 public static Animator getBoundsAnimator(View view, int fromLeft, int fromTop, int fromRight, 271 int fromBottom, int toLeft, int toTop, int toRight, int toBottom) { 272 view.setLeft(fromLeft); 273 view.setTop(fromTop); 274 view.setRight(fromRight); 275 view.setBottom(fromBottom); 276 277 return ObjectAnimator.ofPropertyValuesHolder(view, 278 PropertyValuesHolder.ofInt(VIEW_LEFT, toLeft), 279 PropertyValuesHolder.ofInt(VIEW_TOP, toTop), 280 PropertyValuesHolder.ofInt(VIEW_RIGHT, toRight), 281 PropertyValuesHolder.ofInt(VIEW_BOTTOM, toBottom)); 282 } 283 284 public static void startDrawableAnimation(ImageView view) { 285 final Drawable d = view.getDrawable(); 286 if (d instanceof Animatable) { 287 ((Animatable) d).start(); 288 } 289 } 290 }