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