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