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