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