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