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