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: 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 updateDensity(r); 295 a.recycle(); 296 297 inflateChildElements(r, parser, attrs, theme); 298 299 setFrame(0, true, false); 300 } 301 302 private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, 303 Theme theme) throws XmlPullParserException, IOException { 304 int type; 305 306 final int innerDepth = parser.getDepth()+1; 307 int depth; 308 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 309 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 310 if (type != XmlPullParser.START_TAG) { 311 continue; 312 } 313 314 if (depth > innerDepth || !parser.getName().equals("item")) { 315 continue; 316 } 317 318 final TypedArray a = obtainAttributes(r, theme, attrs, 319 R.styleable.AnimationDrawableItem); 320 321 final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1); 322 if (duration < 0) { 323 throw new XmlPullParserException(parser.getPositionDescription() 324 + ": <item> tag requires a 'duration' attribute"); 325 } 326 327 Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable); 328 329 a.recycle(); 330 331 if (dr == null) { 332 while ((type=parser.next()) == XmlPullParser.TEXT) { 333 // Empty 334 } 335 if (type != XmlPullParser.START_TAG) { 336 throw new XmlPullParserException(parser.getPositionDescription() 337 + ": <item> tag requires a 'drawable' attribute or child tag" 338 + " defining a drawable"); 339 } 340 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 341 } 342 343 mAnimationState.addFrame(dr, duration); 344 if (dr != null) { 345 dr.setCallback(this); 346 } 347 } 348 } 349 350 private void updateStateFromTypedArray(TypedArray a) { 351 mAnimationState.mVariablePadding = a.getBoolean( 352 R.styleable.AnimationDrawable_variablePadding, mAnimationState.mVariablePadding); 353 354 mAnimationState.mOneShot = a.getBoolean( 355 R.styleable.AnimationDrawable_oneshot, mAnimationState.mOneShot); 356 } 357 358 @Override 359 @NonNull 360 public Drawable mutate() { 361 if (!mMutated && super.mutate() == this) { 362 mAnimationState.mutate(); 363 mMutated = true; 364 } 365 return this; 366 } 367 368 @Override 369 AnimationState cloneConstantState() { 370 return new AnimationState(mAnimationState, this, null); 371 } 372 373 /** 374 * @hide 375 */ 376 public void clearMutated() { 377 super.clearMutated(); 378 mMutated = false; 379 } 380 381 private final static class AnimationState extends DrawableContainerState { 382 private int[] mDurations; 383 private boolean mOneShot = false; 384 385 AnimationState(AnimationState orig, AnimationDrawable owner, Resources res) { 386 super(orig, owner, res); 387 388 if (orig != null) { 389 mDurations = orig.mDurations; 390 mOneShot = orig.mOneShot; 391 } else { 392 mDurations = new int[getCapacity()]; 393 mOneShot = false; 394 } 395 } 396 397 private void mutate() { 398 mDurations = mDurations.clone(); 399 } 400 401 @Override 402 public Drawable newDrawable() { 403 return new AnimationDrawable(this, null); 404 } 405 406 @Override 407 public Drawable newDrawable(Resources res) { 408 return new AnimationDrawable(this, res); 409 } 410 411 public void addFrame(Drawable dr, int dur) { 412 // Do not combine the following. The array index must be evaluated before 413 // the array is accessed because super.addChild(dr) has a side effect on mDurations. 414 int pos = super.addChild(dr); 415 mDurations[pos] = dur; 416 } 417 418 @Override 419 public void growArray(int oldSize, int newSize) { 420 super.growArray(oldSize, newSize); 421 int[] newDurations = new int[newSize]; 422 System.arraycopy(mDurations, 0, newDurations, 0, oldSize); 423 mDurations = newDurations; 424 } 425 } 426 427 @Override 428 protected void setConstantState(@NonNull DrawableContainerState state) { 429 super.setConstantState(state); 430 431 if (state instanceof AnimationState) { 432 mAnimationState = (AnimationState) state; 433 } 434 } 435 436 private AnimationDrawable(AnimationState state, Resources res) { 437 final AnimationState as = new AnimationState(state, this, res); 438 setConstantState(as); 439 if (state != null) { 440 setFrame(0, true, false); 441 } 442 } 443} 444 445