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