AnimationDrawable.java revision 084938f2a1b8224bf9ada8a4bc3b48b5cf9275e9
1/* 2 * Copyright (C) 2006 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 17package android.graphics.drawable; 18 19import com.android.internal.R; 20 21import java.io.IOException; 22 23import org.xmlpull.v1.XmlPullParser; 24import org.xmlpull.v1.XmlPullParserException; 25 26import android.annotation.NonNull; 27import android.content.res.Resources; 28import android.content.res.TypedArray; 29import android.content.res.Resources.Theme; 30import android.os.SystemClock; 31import android.util.AttributeSet; 32 33/** 34 * An object used to create frame-by-frame animations, defined by a series of 35 * Drawable objects, which can be used as a View object's background. 36 * <p> 37 * The simplest way to create a frame-by-frame animation is to define the 38 * animation in an XML file, placed in the res/drawable/ folder, and set it as 39 * the background to a View object. Then, call {@link #start()} to run the 40 * animation. 41 * <p> 42 * An AnimationDrawable defined in XML consists of a single 43 * {@code <animation-list>} element and a series of nested 44 * {@code <item>} tags. Each item defines a frame of the animation. See 45 * the example below. 46 * <p> 47 * spin_animation.xml file in res/drawable/ folder: 48 * <pre> 49 * <!-- Animation frames are wheel0.png through wheel5.png 50 * files inside the res/drawable/ folder --> 51 * <animation-list android:id="@+id/selected" android:oneshot="false"> 52 * <item android:drawable="@drawable/wheel0" android:duration="50" /> 53 * <item android:drawable="@drawable/wheel1" android:duration="50" /> 54 * <item android:drawable="@drawable/wheel2" android:duration="50" /> 55 * <item android:drawable="@drawable/wheel3" android:duration="50" /> 56 * <item android:drawable="@drawable/wheel4" android:duration="50" /> 57 * <item android:drawable="@drawable/wheel5" android:duration="50" /> 58 * </animation-list></pre> 59 * <p> 60 * Here is the code to load and play this animation. 61 * <pre> 62 * // Load the ImageView that will host the animation and 63 * // set its background to our AnimationDrawable XML resource. 64 * ImageView img = (ImageView)findViewById(R.id.spinning_wheel_image); 65 * img.setBackgroundResource(R.drawable.spin_animation); 66 * 67 * // Get the background, which has been compiled to an AnimationDrawable object. 68 * AnimationDrawable frameAnimation = (AnimationDrawable) img.getBackground(); 69 * 70 * // Start the animation (looped playback by default). 71 * frameAnimation.start(); 72 * </pre> 73 * 74 * <div class="special reference"> 75 * <h3>Developer Guides</h3> 76 * <p>For more information about animating with {@code AnimationDrawable}, read the 77 * <a href="{@docRoot}guide/topics/graphics/drawable-animation.html">Drawable Animation</a> 78 * developer guide.</p> 79 * </div> 80 * 81 * @attr ref android.R.styleable#AnimationDrawable_visible 82 * @attr ref android.R.styleable#AnimationDrawable_variablePadding 83 * @attr ref android.R.styleable#AnimationDrawable_oneshot 84 * @attr ref android.R.styleable#AnimationDrawableItem_duration 85 * @attr ref android.R.styleable#AnimationDrawableItem_drawable 86 */ 87public class AnimationDrawable extends DrawableContainer implements Runnable, Animatable { 88 private AnimationState mAnimationState; 89 90 /** The current frame, ranging from 0 to {@link #mAnimationState#getChildCount() - 1} */ 91 private int mCurFrame = 0; 92 93 /** Whether the drawable has an animation callback posted. */ 94 private boolean mRunning; 95 96 /** Whether the drawable should animate when visible. */ 97 private boolean mAnimating; 98 99 private boolean mMutated; 100 101 public AnimationDrawable() { 102 this(null, null); 103 } 104 105 /** 106 * Sets whether this AnimationDrawable is visible. 107 * <p> 108 * When the drawable becomes invisible, it will pause its animation. A subsequent change to 109 * visible with <code>restart</code> set to true will restart the animation from the 110 * first frame. If <code>restart</code> is false, the drawable will resume from the most recent 111 * frame. If the drawable has already reached the last frame, it will then loop back to the 112 * first frame, unless it's a one shot drawable (set through {@link #setOneShot(boolean)}), 113 * in which case, it will stay on the last frame. 114 * 115 * @param visible true if visible, false otherwise 116 * @param restart when visible, true to force the animation to restart 117 * from the first frame 118 * @return true if the new visibility is different than its previous state 119 */ 120 @Override 121 public boolean setVisible(boolean visible, boolean restart) { 122 final boolean changed = super.setVisible(visible, restart); 123 if (visible) { 124 if (restart || changed) { 125 boolean startFromZero = restart || (!mRunning && !mAnimationState.mOneShot) || 126 mCurFrame >= mAnimationState.getChildCount(); 127 setFrame(startFromZero ? 0 : mCurFrame, true, mAnimating); 128 } 129 } else { 130 unscheduleSelf(this); 131 } 132 return changed; 133 } 134 135 /** 136 * Starts the animation from the first frame, looping if necessary. This method has no effect 137 * if the animation is running. 138 * <p> 139 * <strong>Note:</strong> Do not call this in the 140 * {@link android.app.Activity#onCreate} method of your activity, because 141 * the {@link AnimationDrawable} is not yet fully attached to the window. 142 * If you want to play the animation immediately without requiring 143 * interaction, then you might want to call it from the 144 * {@link android.app.Activity#onWindowFocusChanged} method in your 145 * activity, which will get called when Android brings your window into 146 * focus. 147 * 148 * @see #isRunning() 149 * @see #stop() 150 */ 151 @Override 152 public void start() { 153 mAnimating = true; 154 155 if (!isRunning()) { 156 // Start from 0th frame. 157 setFrame(0, false, mAnimationState.getChildCount() > 1 158 || !mAnimationState.mOneShot); 159 } 160 } 161 162 /** 163 * Stops the animation at the current frame. This method has no effect if the animation is not 164 * running. 165 * 166 * @see #isRunning() 167 * @see #start() 168 */ 169 @Override 170 public void stop() { 171 mAnimating = false; 172 173 if (isRunning()) { 174 mCurFrame = 0; 175 unscheduleSelf(this); 176 } 177 } 178 179 /** 180 * Indicates whether the animation is currently running or not. 181 * 182 * @return true if the animation is running, false otherwise 183 */ 184 @Override 185 public boolean isRunning() { 186 return mRunning; 187 } 188 189 /** 190 * This method exists for implementation purpose only and should not be 191 * called directly. Invoke {@link #start()} instead. 192 * 193 * @see #start() 194 */ 195 @Override 196 public void run() { 197 nextFrame(false); 198 } 199 200 @Override 201 public void unscheduleSelf(Runnable what) { 202 mRunning = false; 203 super.unscheduleSelf(what); 204 } 205 206 /** 207 * @return The number of frames in the animation 208 */ 209 public int getNumberOfFrames() { 210 return mAnimationState.getChildCount(); 211 } 212 213 /** 214 * @return The Drawable at the specified frame index 215 */ 216 public Drawable getFrame(int index) { 217 return mAnimationState.getChild(index); 218 } 219 220 /** 221 * @return The duration in milliseconds of the frame at the 222 * specified index 223 */ 224 public int getDuration(int i) { 225 return mAnimationState.mDurations[i]; 226 } 227 228 /** 229 * @return True of the animation will play once, false otherwise 230 */ 231 public boolean isOneShot() { 232 return mAnimationState.mOneShot; 233 } 234 235 /** 236 * Sets whether the animation should play once or repeat. 237 * 238 * @param oneShot Pass true if the animation should only play once 239 */ 240 public void setOneShot(boolean oneShot) { 241 mAnimationState.mOneShot = oneShot; 242 } 243 244 /** 245 * Adds a frame to the animation 246 * 247 * @param frame The frame to add 248 * @param duration How long in milliseconds the frame should appear 249 */ 250 public void addFrame(@NonNull Drawable frame, int duration) { 251 mAnimationState.addFrame(frame, duration); 252 if (!mRunning) { 253 setFrame(0, true, false); 254 } 255 } 256 257 private void nextFrame(boolean unschedule) { 258 int nextFrame = mCurFrame + 1; 259 final int numFrames = mAnimationState.getChildCount(); 260 final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1); 261 262 // Loop if necessary. One-shot animations should never hit this case. 263 if (!mAnimationState.mOneShot && nextFrame >= numFrames) { 264 nextFrame = 0; 265 } 266 267 setFrame(nextFrame, unschedule, !isLastFrame); 268 } 269 270 private void setFrame(int frame, boolean unschedule, boolean animate) { 271 if (frame >= mAnimationState.getChildCount()) { 272 return; 273 } 274 mAnimating = animate; 275 mCurFrame = frame; 276 selectDrawable(frame); 277 if (unschedule || animate) { 278 unscheduleSelf(this); 279 } 280 if (animate) { 281 // Unscheduling may have clobbered these values; restore them 282 mCurFrame = frame; 283 mRunning = true; 284 scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]); 285 } 286 } 287 288 @Override 289 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 290 throws XmlPullParserException, IOException { 291 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable); 292 super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible); 293 updateStateFromTypedArray(a); 294 a.recycle(); 295 296 inflateChildElements(r, parser, attrs, theme); 297 298 setFrame(0, true, false); 299 } 300 301 private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, 302 Theme theme) throws XmlPullParserException, IOException { 303 int type; 304 305 final int innerDepth = parser.getDepth()+1; 306 int depth; 307 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 308 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 309 if (type != XmlPullParser.START_TAG) { 310 continue; 311 } 312 313 if (depth > innerDepth || !parser.getName().equals("item")) { 314 continue; 315 } 316 317 final TypedArray a = obtainAttributes(r, theme, attrs, 318 R.styleable.AnimationDrawableItem); 319 320 final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1); 321 if (duration < 0) { 322 throw new XmlPullParserException(parser.getPositionDescription() 323 + ": <item> tag requires a 'duration' attribute"); 324 } 325 326 Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable); 327 328 a.recycle(); 329 330 if (dr == null) { 331 while ((type=parser.next()) == XmlPullParser.TEXT) { 332 // Empty 333 } 334 if (type != XmlPullParser.START_TAG) { 335 throw new XmlPullParserException(parser.getPositionDescription() 336 + ": <item> tag requires a 'drawable' attribute or child tag" 337 + " defining a drawable"); 338 } 339 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 340 } 341 342 mAnimationState.addFrame(dr, duration); 343 if (dr != null) { 344 dr.setCallback(this); 345 } 346 } 347 } 348 349 private void updateStateFromTypedArray(TypedArray a) { 350 mAnimationState.mVariablePadding = a.getBoolean( 351 R.styleable.AnimationDrawable_variablePadding, mAnimationState.mVariablePadding); 352 353 mAnimationState.mOneShot = a.getBoolean( 354 R.styleable.AnimationDrawable_oneshot, mAnimationState.mOneShot); 355 } 356 357 @Override 358 @NonNull 359 public Drawable mutate() { 360 if (!mMutated && super.mutate() == this) { 361 mAnimationState.mutate(); 362 mMutated = true; 363 } 364 return this; 365 } 366 367 @Override 368 AnimationState cloneConstantState() { 369 return new AnimationState(mAnimationState, this, null); 370 } 371 372 /** 373 * @hide 374 */ 375 public void clearMutated() { 376 super.clearMutated(); 377 mMutated = false; 378 } 379 380 private final static class AnimationState extends DrawableContainerState { 381 private int[] mDurations; 382 private boolean mOneShot = false; 383 384 AnimationState(AnimationState orig, AnimationDrawable owner, Resources res) { 385 super(orig, owner, res); 386 387 if (orig != null) { 388 mDurations = orig.mDurations; 389 mOneShot = orig.mOneShot; 390 } else { 391 mDurations = new int[getCapacity()]; 392 mOneShot = false; 393 } 394 } 395 396 private void mutate() { 397 mDurations = mDurations.clone(); 398 } 399 400 @Override 401 public Drawable newDrawable() { 402 return new AnimationDrawable(this, null); 403 } 404 405 @Override 406 public Drawable newDrawable(Resources res) { 407 return new AnimationDrawable(this, res); 408 } 409 410 public void addFrame(Drawable dr, int dur) { 411 // Do not combine the following. The array index must be evaluated before 412 // the array is accessed because super.addChild(dr) has a side effect on mDurations. 413 int pos = super.addChild(dr); 414 mDurations[pos] = dur; 415 } 416 417 @Override 418 public void growArray(int oldSize, int newSize) { 419 super.growArray(oldSize, newSize); 420 int[] newDurations = new int[newSize]; 421 System.arraycopy(mDurations, 0, newDurations, 0, oldSize); 422 mDurations = newDurations; 423 } 424 } 425 426 @Override 427 protected void setConstantState(@NonNull DrawableContainerState state) { 428 super.setConstantState(state); 429 430 if (state instanceof AnimationState) { 431 mAnimationState = (AnimationState) state; 432 } 433 } 434 435 private AnimationDrawable(AnimationState state, Resources res) { 436 final AnimationState as = new AnimationState(state, this, res); 437 setConstantState(as); 438 if (state != null) { 439 setFrame(0, true, false); 440 } 441 } 442} 443 444