1 /*
2  * Copyright (C) 2007 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 android.view.animation;
18 
19 import android.annotation.AnimRes;
20 import android.annotation.InterpolatorRes;
21 import android.annotation.TestApi;
22 import android.compat.annotation.UnsupportedAppUsage;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.content.res.Resources.NotFoundException;
26 import android.content.res.Resources.Theme;
27 import android.content.res.XmlResourceParser;
28 import android.os.SystemClock;
29 import android.util.AttributeSet;
30 import android.util.Xml;
31 
32 import org.xmlpull.v1.XmlPullParser;
33 import org.xmlpull.v1.XmlPullParserException;
34 
35 import java.io.IOException;
36 
37 /**
38  * Defines common utilities for working with animations.
39  *
40  */
41 public class AnimationUtils {
42 
43     /**
44      * These flags are used when parsing AnimatorSet objects
45      */
46     private static final int TOGETHER = 0;
47     private static final int SEQUENTIALLY = 1;
48 
49     private static class AnimationState {
50         boolean animationClockLocked;
51         long currentVsyncTimeMillis;
52         long lastReportedTimeMillis;
53     };
54 
55     private static ThreadLocal<AnimationState> sAnimationState
56             = new ThreadLocal<AnimationState>() {
57         @Override
58         protected AnimationState initialValue() {
59             return new AnimationState();
60         }
61     };
62 
63     /**
64      * Locks AnimationUtils{@link #currentAnimationTimeMillis()} to a fixed value for the current
65      * thread. This is used by {@link android.view.Choreographer} to ensure that all accesses
66      * during a vsync update are synchronized to the timestamp of the vsync.
67      *
68      * It is also exposed to tests to allow for rapid, flake-free headless testing.
69      *
70      * Must be followed by a call to {@link #unlockAnimationClock()} to allow time to
71      * progress. Failing to do this will result in stuck animations, scrolls, and flings.
72      *
73      * Note that time is not allowed to "rewind" and must perpetually flow forward. So the
74      * lock may fail if the time is in the past from a previously returned value, however
75      * time will be frozen for the duration of the lock. The clock is a thread-local, so
76      * ensure that {@link #lockAnimationClock(long)}, {@link #unlockAnimationClock()}, and
77      * {@link #currentAnimationTimeMillis()} are all called on the same thread.
78      *
79      * This is also not reference counted in any way. Any call to {@link #unlockAnimationClock()}
80      * will unlock the clock for everyone on the same thread. It is therefore recommended
81      * for tests to use their own thread to ensure that there is no collision with any existing
82      * {@link android.view.Choreographer} instance.
83      *
84      * @hide
85      * */
86     @TestApi
lockAnimationClock(long vsyncMillis)87     public static void lockAnimationClock(long vsyncMillis) {
88         AnimationState state = sAnimationState.get();
89         state.animationClockLocked = true;
90         state.currentVsyncTimeMillis = vsyncMillis;
91     }
92 
93     /**
94      * Frees the time lock set in place by {@link #lockAnimationClock(long)}. Must be called
95      * to allow the animation clock to self-update.
96      *
97      * @hide
98      */
99     @TestApi
unlockAnimationClock()100     public static void unlockAnimationClock() {
101         sAnimationState.get().animationClockLocked = false;
102     }
103 
104     /**
105      * Returns the current animation time in milliseconds. This time should be used when invoking
106      * {@link Animation#setStartTime(long)}. Refer to {@link android.os.SystemClock} for more
107      * information about the different available clocks. The clock used by this method is
108      * <em>not</em> the "wall" clock (it is not {@link System#currentTimeMillis}).
109      *
110      * @return the current animation time in milliseconds
111      *
112      * @see android.os.SystemClock
113      */
currentAnimationTimeMillis()114     public static long currentAnimationTimeMillis() {
115         AnimationState state = sAnimationState.get();
116         if (state.animationClockLocked) {
117             // It's important that time never rewinds
118             return Math.max(state.currentVsyncTimeMillis,
119                     state.lastReportedTimeMillis);
120         }
121         state.lastReportedTimeMillis = SystemClock.uptimeMillis();
122         return state.lastReportedTimeMillis;
123     }
124 
125     /**
126      * Loads an {@link Animation} object from a resource
127      *
128      * @param context Application context used to access resources
129      * @param id The resource id of the animation to load
130      * @return The animation object referenced by the specified id
131      * @throws NotFoundException when the animation cannot be loaded
132      */
loadAnimation(Context context, @AnimRes int id)133     public static Animation loadAnimation(Context context, @AnimRes int id)
134             throws NotFoundException {
135 
136         XmlResourceParser parser = null;
137         try {
138             parser = context.getResources().getAnimation(id);
139             return createAnimationFromXml(context, parser);
140         } catch (XmlPullParserException ex) {
141             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
142                     Integer.toHexString(id));
143             rnf.initCause(ex);
144             throw rnf;
145         } catch (IOException ex) {
146             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
147                     Integer.toHexString(id));
148             rnf.initCause(ex);
149             throw rnf;
150         } finally {
151             if (parser != null) parser.close();
152         }
153     }
154 
createAnimationFromXml(Context c, XmlPullParser parser)155     private static Animation createAnimationFromXml(Context c, XmlPullParser parser)
156             throws XmlPullParserException, IOException {
157 
158         return createAnimationFromXml(c, parser, null, Xml.asAttributeSet(parser));
159     }
160 
161     @UnsupportedAppUsage
createAnimationFromXml(Context c, XmlPullParser parser, AnimationSet parent, AttributeSet attrs)162     private static Animation createAnimationFromXml(Context c, XmlPullParser parser,
163             AnimationSet parent, AttributeSet attrs) throws XmlPullParserException, IOException {
164 
165         Animation anim = null;
166 
167         // Make sure we are on a start tag.
168         int type;
169         int depth = parser.getDepth();
170 
171         while (((type=parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
172                && type != XmlPullParser.END_DOCUMENT) {
173 
174             if (type != XmlPullParser.START_TAG) {
175                 continue;
176             }
177 
178             String  name = parser.getName();
179 
180             if (name.equals("set")) {
181                 anim = new AnimationSet(c, attrs);
182                 createAnimationFromXml(c, parser, (AnimationSet)anim, attrs);
183             } else if (name.equals("alpha")) {
184                 anim = new AlphaAnimation(c, attrs);
185             } else if (name.equals("scale")) {
186                 anim = new ScaleAnimation(c, attrs);
187             }  else if (name.equals("rotate")) {
188                 anim = new RotateAnimation(c, attrs);
189             }  else if (name.equals("translate")) {
190                 anim = new TranslateAnimation(c, attrs);
191             } else if (name.equals("cliprect")) {
192                 anim = new ClipRectAnimation(c, attrs);
193             } else {
194                 throw new RuntimeException("Unknown animation name: " + parser.getName());
195             }
196 
197             if (parent != null) {
198                 parent.addAnimation(anim);
199             }
200         }
201 
202         return anim;
203 
204     }
205 
206     /**
207      * Loads a {@link LayoutAnimationController} object from a resource
208      *
209      * @param context Application context used to access resources
210      * @param id The resource id of the animation to load
211      * @return The animation controller object referenced by the specified id
212      * @throws NotFoundException when the layout animation controller cannot be loaded
213      */
loadLayoutAnimation(Context context, @AnimRes int id)214     public static LayoutAnimationController loadLayoutAnimation(Context context, @AnimRes int id)
215             throws NotFoundException {
216 
217         XmlResourceParser parser = null;
218         try {
219             parser = context.getResources().getAnimation(id);
220             return createLayoutAnimationFromXml(context, parser);
221         } catch (XmlPullParserException ex) {
222             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
223                     Integer.toHexString(id));
224             rnf.initCause(ex);
225             throw rnf;
226         } catch (IOException ex) {
227             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
228                     Integer.toHexString(id));
229             rnf.initCause(ex);
230             throw rnf;
231         } finally {
232             if (parser != null) parser.close();
233         }
234     }
235 
createLayoutAnimationFromXml(Context c, XmlPullParser parser)236     private static LayoutAnimationController createLayoutAnimationFromXml(Context c,
237             XmlPullParser parser) throws XmlPullParserException, IOException {
238 
239         return createLayoutAnimationFromXml(c, parser, Xml.asAttributeSet(parser));
240     }
241 
createLayoutAnimationFromXml(Context c, XmlPullParser parser, AttributeSet attrs)242     private static LayoutAnimationController createLayoutAnimationFromXml(Context c,
243             XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
244 
245         LayoutAnimationController controller = null;
246 
247         int type;
248         int depth = parser.getDepth();
249 
250         while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
251                 && type != XmlPullParser.END_DOCUMENT) {
252 
253             if (type != XmlPullParser.START_TAG) {
254                 continue;
255             }
256 
257             String name = parser.getName();
258 
259             if ("layoutAnimation".equals(name)) {
260                 controller = new LayoutAnimationController(c, attrs);
261             } else if ("gridLayoutAnimation".equals(name)) {
262                 controller = new GridLayoutAnimationController(c, attrs);
263             } else {
264                 throw new RuntimeException("Unknown layout animation name: " + name);
265             }
266         }
267 
268         return controller;
269     }
270 
271     /**
272      * Make an animation for objects becoming visible. Uses a slide and fade
273      * effect.
274      *
275      * @param c Context for loading resources
276      * @param fromLeft is the object to be animated coming from the left
277      * @return The new animation
278      */
makeInAnimation(Context c, boolean fromLeft)279     public static Animation makeInAnimation(Context c, boolean fromLeft) {
280         Animation a;
281         if (fromLeft) {
282             a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_left);
283         } else {
284             a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_right);
285         }
286 
287         a.setInterpolator(new DecelerateInterpolator());
288         a.setStartTime(currentAnimationTimeMillis());
289         return a;
290     }
291 
292     /**
293      * Make an animation for objects becoming invisible. Uses a slide and fade
294      * effect.
295      *
296      * @param c Context for loading resources
297      * @param toRight is the object to be animated exiting to the right
298      * @return The new animation
299      */
makeOutAnimation(Context c, boolean toRight)300     public static Animation makeOutAnimation(Context c, boolean toRight) {
301         Animation a;
302         if (toRight) {
303             a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_out_right);
304         } else {
305             a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_out_left);
306         }
307 
308         a.setInterpolator(new AccelerateInterpolator());
309         a.setStartTime(currentAnimationTimeMillis());
310         return a;
311     }
312 
313 
314     /**
315      * Make an animation for objects becoming visible. Uses a slide up and fade
316      * effect.
317      *
318      * @param c Context for loading resources
319      * @return The new animation
320      */
makeInChildBottomAnimation(Context c)321     public static Animation makeInChildBottomAnimation(Context c) {
322         Animation a;
323         a = AnimationUtils.loadAnimation(c, com.android.internal.R.anim.slide_in_child_bottom);
324         a.setInterpolator(new AccelerateInterpolator());
325         a.setStartTime(currentAnimationTimeMillis());
326         return a;
327     }
328 
329     /**
330      * Loads an {@link Interpolator} object from a resource
331      *
332      * @param context Application context used to access resources
333      * @param id The resource id of the animation to load
334      * @return The interpolator object referenced by the specified id
335      * @throws NotFoundException
336      */
loadInterpolator(Context context, @AnimRes @InterpolatorRes int id)337     public static Interpolator loadInterpolator(Context context, @AnimRes @InterpolatorRes int id)
338             throws NotFoundException {
339         XmlResourceParser parser = null;
340         try {
341             parser = context.getResources().getAnimation(id);
342             return createInterpolatorFromXml(context.getResources(), context.getTheme(), parser);
343         } catch (XmlPullParserException ex) {
344             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
345                     Integer.toHexString(id));
346             rnf.initCause(ex);
347             throw rnf;
348         } catch (IOException ex) {
349             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
350                     Integer.toHexString(id));
351             rnf.initCause(ex);
352             throw rnf;
353         } finally {
354             if (parser != null) parser.close();
355         }
356 
357     }
358 
359     /**
360      * Loads an {@link Interpolator} object from a resource
361      *
362      * @param res The resources
363      * @param id The resource id of the animation to load
364      * @return The interpolator object referenced by the specified id
365      * @throws NotFoundException
366      * @hide
367      */
loadInterpolator(Resources res, Theme theme, int id)368     public static Interpolator loadInterpolator(Resources res, Theme theme, int id) throws NotFoundException {
369         XmlResourceParser parser = null;
370         try {
371             parser = res.getAnimation(id);
372             return createInterpolatorFromXml(res, theme, parser);
373         } catch (XmlPullParserException ex) {
374             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
375                     Integer.toHexString(id));
376             rnf.initCause(ex);
377             throw rnf;
378         } catch (IOException ex) {
379             NotFoundException rnf = new NotFoundException("Can't load animation resource ID #0x" +
380                     Integer.toHexString(id));
381             rnf.initCause(ex);
382             throw rnf;
383         } finally {
384             if (parser != null)
385                 parser.close();
386         }
387 
388     }
389 
createInterpolatorFromXml(Resources res, Theme theme, XmlPullParser parser)390     private static Interpolator createInterpolatorFromXml(Resources res, Theme theme, XmlPullParser parser)
391             throws XmlPullParserException, IOException {
392 
393         BaseInterpolator interpolator = null;
394 
395         // Make sure we are on a start tag.
396         int type;
397         int depth = parser.getDepth();
398 
399         while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
400                 && type != XmlPullParser.END_DOCUMENT) {
401 
402             if (type != XmlPullParser.START_TAG) {
403                 continue;
404             }
405 
406             AttributeSet attrs = Xml.asAttributeSet(parser);
407 
408             String name = parser.getName();
409 
410             if (name.equals("linearInterpolator")) {
411                 interpolator = new LinearInterpolator();
412             } else if (name.equals("accelerateInterpolator")) {
413                 interpolator = new AccelerateInterpolator(res, theme, attrs);
414             } else if (name.equals("decelerateInterpolator")) {
415                 interpolator = new DecelerateInterpolator(res, theme, attrs);
416             } else if (name.equals("accelerateDecelerateInterpolator")) {
417                 interpolator = new AccelerateDecelerateInterpolator();
418             } else if (name.equals("cycleInterpolator")) {
419                 interpolator = new CycleInterpolator(res, theme, attrs);
420             } else if (name.equals("anticipateInterpolator")) {
421                 interpolator = new AnticipateInterpolator(res, theme, attrs);
422             } else if (name.equals("overshootInterpolator")) {
423                 interpolator = new OvershootInterpolator(res, theme, attrs);
424             } else if (name.equals("anticipateOvershootInterpolator")) {
425                 interpolator = new AnticipateOvershootInterpolator(res, theme, attrs);
426             } else if (name.equals("bounceInterpolator")) {
427                 interpolator = new BounceInterpolator();
428             } else if (name.equals("pathInterpolator")) {
429                 interpolator = new PathInterpolator(res, theme, attrs);
430             } else {
431                 throw new RuntimeException("Unknown interpolator name: " + parser.getName());
432             }
433         }
434         return interpolator;
435     }
436 }
437