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 109 * subsequent change to visible with <code>restart</code> set to true will 110 * restart the animation from the first frame. If <code>restart</code> is 111 * false, the animation will resume from the most recent frame. 112 * 113 * @param visible true if visible, false otherwise 114 * @param restart when visible, true to force the animation to restart 115 * from the first frame 116 * @return true if the new visibility is different than its previous state 117 */ 118 @Override 119 public boolean setVisible(boolean visible, boolean restart) { 120 final boolean changed = super.setVisible(visible, restart); 121 if (visible) { 122 if (restart || changed) { 123 boolean startFromZero = restart || !mRunning || 124 mCurFrame >= mAnimationState.getChildCount(); 125 setFrame(startFromZero ? 0 : mCurFrame, true, mAnimating); 126 } 127 } else { 128 unscheduleSelf(this); 129 } 130 return changed; 131 } 132 133 /** 134 * Starts the animation, looping if necessary. This method has no effect 135 * if the animation is running. 136 * <p> 137 * <strong>Note:</strong> Do not call this in the 138 * {@link android.app.Activity#onCreate} method of your activity, because 139 * the {@link AnimationDrawable} is not yet fully attached to the window. 140 * If you want to play the animation immediately without requiring 141 * interaction, then you might want to call it from the 142 * {@link android.app.Activity#onWindowFocusChanged} method in your 143 * activity, which will get called when Android brings your window into 144 * focus. 145 * 146 * @see #isRunning() 147 * @see #stop() 148 */ 149 @Override 150 public void start() { 151 mAnimating = true; 152 153 if (!isRunning()) { 154 // Start from 0th frame. 155 setFrame(0, false, mAnimationState.getChildCount() > 1 156 || !mAnimationState.mOneShot); 157 } 158 } 159 160 /** 161 * Stops the animation. This method has no effect if the animation is not 162 * running. 163 * 164 * @see #isRunning() 165 * @see #start() 166 */ 167 @Override 168 public void stop() { 169 mAnimating = false; 170 171 if (isRunning()) { 172 unscheduleSelf(this); 173 } 174 } 175 176 /** 177 * Indicates whether the animation is currently running or not. 178 * 179 * @return true if the animation is running, false otherwise 180 */ 181 @Override 182 public boolean isRunning() { 183 return mRunning; 184 } 185 186 /** 187 * This method exists for implementation purpose only and should not be 188 * called directly. Invoke {@link #start()} instead. 189 * 190 * @see #start() 191 */ 192 @Override 193 public void run() { 194 nextFrame(false); 195 } 196 197 @Override 198 public void unscheduleSelf(Runnable what) { 199 mCurFrame = 0; 200 mRunning = false; 201 super.unscheduleSelf(what); 202 } 203 204 /** 205 * @return The number of frames in the animation 206 */ 207 public int getNumberOfFrames() { 208 return mAnimationState.getChildCount(); 209 } 210 211 /** 212 * @return The Drawable at the specified frame index 213 */ 214 public Drawable getFrame(int index) { 215 return mAnimationState.getChild(index); 216 } 217 218 /** 219 * @return The duration in milliseconds of the frame at the 220 * specified index 221 */ 222 public int getDuration(int i) { 223 return mAnimationState.mDurations[i]; 224 } 225 226 /** 227 * @return True of the animation will play once, false otherwise 228 */ 229 public boolean isOneShot() { 230 return mAnimationState.mOneShot; 231 } 232 233 /** 234 * Sets whether the animation should play once or repeat. 235 * 236 * @param oneShot Pass true if the animation should only play once 237 */ 238 public void setOneShot(boolean oneShot) { 239 mAnimationState.mOneShot = oneShot; 240 } 241 242 /** 243 * Adds a frame to the animation 244 * 245 * @param frame The frame to add 246 * @param duration How long in milliseconds the frame should appear 247 */ 248 public void addFrame(@NonNull Drawable frame, int duration) { 249 mAnimationState.addFrame(frame, duration); 250 if (!mRunning) { 251 setFrame(0, true, false); 252 } 253 } 254 255 private void nextFrame(boolean unschedule) { 256 int nextFrame = mCurFrame + 1; 257 final int numFrames = mAnimationState.getChildCount(); 258 final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1); 259 260 // Loop if necessary. One-shot animations should never hit this case. 261 if (!mAnimationState.mOneShot && nextFrame >= numFrames) { 262 nextFrame = 0; 263 } 264 265 setFrame(nextFrame, unschedule, !isLastFrame); 266 } 267 268 private void setFrame(int frame, boolean unschedule, boolean animate) { 269 if (frame >= mAnimationState.getChildCount()) { 270 return; 271 } 272 mAnimating = animate; 273 mCurFrame = frame; 274 selectDrawable(frame); 275 if (unschedule || animate) { 276 unscheduleSelf(this); 277 } 278 if (animate) { 279 // Unscheduling may have clobbered these values; restore them 280 mCurFrame = frame; 281 mRunning = true; 282 scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]); 283 } 284 } 285 286 @Override 287 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 288 throws XmlPullParserException, IOException { 289 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable); 290 super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible); 291 updateStateFromTypedArray(a); 292 a.recycle(); 293 294 inflateChildElements(r, parser, attrs, theme); 295 296 setFrame(0, true, false); 297 } 298 299 private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, 300 Theme theme) throws XmlPullParserException, IOException { 301 int type; 302 303 final int innerDepth = parser.getDepth()+1; 304 int depth; 305 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT 306 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 307 if (type != XmlPullParser.START_TAG) { 308 continue; 309 } 310 311 if (depth > innerDepth || !parser.getName().equals("item")) { 312 continue; 313 } 314 315 final TypedArray a = obtainAttributes(r, theme, attrs, 316 R.styleable.AnimationDrawableItem); 317 318 final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1); 319 if (duration < 0) { 320 throw new XmlPullParserException(parser.getPositionDescription() 321 + ": <item> tag requires a 'duration' attribute"); 322 } 323 324 Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable); 325 326 a.recycle(); 327 328 if (dr == null) { 329 while ((type=parser.next()) == XmlPullParser.TEXT) { 330 // Empty 331 } 332 if (type != XmlPullParser.START_TAG) { 333 throw new XmlPullParserException(parser.getPositionDescription() 334 + ": <item> tag requires a 'drawable' attribute or child tag" 335 + " defining a drawable"); 336 } 337 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 338 } 339 340 mAnimationState.addFrame(dr, duration); 341 if (dr != null) { 342 dr.setCallback(this); 343 } 344 } 345 } 346 347 private void updateStateFromTypedArray(TypedArray a) { 348 mAnimationState.mVariablePadding = a.getBoolean( 349 R.styleable.AnimationDrawable_variablePadding, mAnimationState.mVariablePadding); 350 351 mAnimationState.mOneShot = a.getBoolean( 352 R.styleable.AnimationDrawable_oneshot, mAnimationState.mOneShot); 353 } 354 355 @Override 356 @NonNull 357 public Drawable mutate() { 358 if (!mMutated && super.mutate() == this) { 359 mAnimationState.mutate(); 360 mMutated = true; 361 } 362 return this; 363 } 364 365 @Override 366 AnimationState cloneConstantState() { 367 return new AnimationState(mAnimationState, this, null); 368 } 369 370 /** 371 * @hide 372 */ 373 public void clearMutated() { 374 super.clearMutated(); 375 mMutated = false; 376 } 377 378 private final static class AnimationState extends DrawableContainerState { 379 private int[] mDurations; 380 private boolean mOneShot = false; 381 382 AnimationState(AnimationState orig, AnimationDrawable owner, Resources res) { 383 super(orig, owner, res); 384 385 if (orig != null) { 386 mDurations = orig.mDurations; 387 mOneShot = orig.mOneShot; 388 } else { 389 mDurations = new int[getCapacity()]; 390 mOneShot = false; 391 } 392 } 393 394 private void mutate() { 395 mDurations = mDurations.clone(); 396 } 397 398 @Override 399 public Drawable newDrawable() { 400 return new AnimationDrawable(this, null); 401 } 402 403 @Override 404 public Drawable newDrawable(Resources res) { 405 return new AnimationDrawable(this, res); 406 } 407 408 public void addFrame(Drawable dr, int dur) { 409 // Do not combine the following. The array index must be evaluated before 410 // the array is accessed because super.addChild(dr) has a side effect on mDurations. 411 int pos = super.addChild(dr); 412 mDurations[pos] = dur; 413 } 414 415 @Override 416 public void growArray(int oldSize, int newSize) { 417 super.growArray(oldSize, newSize); 418 int[] newDurations = new int[newSize]; 419 System.arraycopy(mDurations, 0, newDurations, 0, oldSize); 420 mDurations = newDurations; 421 } 422 } 423 424 @Override 425 protected void setConstantState(@NonNull DrawableContainerState state) { 426 super.setConstantState(state); 427 428 if (state instanceof AnimationState) { 429 mAnimationState = (AnimationState) state; 430 } 431 } 432 433 private AnimationDrawable(AnimationState state, Resources res) { 434 final AnimationState as = new AnimationState(state, this, res); 435 setConstantState(as); 436 if (state != null) { 437 setFrame(0, true, false); 438 } 439 } 440} 441 442