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