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