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 androidx.core.graphics.drawable.DrawableCompat;
30 import androidx.interpolator.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      */
setBackgroundAlpha(View view, Integer value)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 
setAnimatedFraction(ValueAnimator animator, float fraction)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 
reverse(ValueAnimator... animators)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 
cancel(ValueAnimator... animators)165     public static void cancel(ValueAnimator... animators) {
166         for (ValueAnimator animator : animators) {
167             animator.cancel();
168         }
169     }
170 
getScaleAnimator(View view, float... values)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 
getAlphaAnimator(View view, float... values)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      */
getBoundsAnimator(View target, View from, View to)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      */
getBoundsAnimator(View view, int fromLeft, int fromTop, int fromRight, int fromBottom, int toLeft, int toTop, int toRight, int toBottom)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 
startDrawableAnimation(ImageView view)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 }