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 java.io.IOException; 20 21 import org.xmlpull.v1.XmlPullParser; 22 import org.xmlpull.v1.XmlPullParserException; 23 24 import android.content.res.Resources; 25 import android.content.res.TypedArray; 26 import android.os.SystemClock; 27 import android.util.AttributeSet; 28 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 */ 81 public class AnimationDrawable extends DrawableContainer implements Runnable, Animatable { 82 private final AnimationState mAnimationState; 83 private int mCurFrame = -1; 84 private boolean mMutated; 85 86 public AnimationDrawable() { 87 this(null, null); 88 } 89 90 @Override 91 public boolean setVisible(boolean visible, boolean restart) { 92 boolean changed = super.setVisible(visible, restart); 93 if (visible) { 94 if (changed || restart) { 95 setFrame(0, true, true); 96 } 97 } else { 98 unscheduleSelf(this); 99 } 100 return changed; 101 } 102 103 /** 104 * <p>Starts the animation, looping if necessary. This method has no effect 105 * if the animation is running. Do not call this in the {@link android.app.Activity#onCreate} 106 * method of your activity, because the {@link android.graphics.drawable.AnimationDrawable} is 107 * not yet fully attached to the window. If you want to play 108 * the animation immediately, without requiring interaction, then you might want to call it 109 * from the {@link android.app.Activity#onWindowFocusChanged} method in your activity, 110 * which will get called when Android brings your window into focus.</p> 111 * 112 * @see #isRunning() 113 * @see #stop() 114 */ 115 public void start() { 116 if (!isRunning()) { 117 run(); 118 } 119 } 120 121 /** 122 * <p>Stops the animation. This method has no effect if the animation is 123 * not running.</p> 124 * 125 * @see #isRunning() 126 * @see #start() 127 */ 128 public void stop() { 129 if (isRunning()) { 130 unscheduleSelf(this); 131 } 132 } 133 134 /** 135 * <p>Indicates whether the animation is currently running or not.</p> 136 * 137 * @return true if the animation is running, false otherwise 138 */ 139 public boolean isRunning() { 140 return mCurFrame > -1; 141 } 142 143 /** 144 * <p>This method exists for implementation purpose only and should not be 145 * called directly. Invoke {@link #start()} instead.</p> 146 * 147 * @see #start() 148 */ 149 public void run() { 150 nextFrame(false); 151 } 152 153 @Override 154 public void unscheduleSelf(Runnable what) { 155 mCurFrame = -1; 156 super.unscheduleSelf(what); 157 } 158 159 /** 160 * @return The number of frames in the animation 161 */ 162 public int getNumberOfFrames() { 163 return mAnimationState.getChildCount(); 164 } 165 166 /** 167 * @return The Drawable at the specified frame index 168 */ 169 public Drawable getFrame(int index) { 170 return mAnimationState.getChild(index); 171 } 172 173 /** 174 * @return The duration in milliseconds of the frame at the 175 * specified index 176 */ 177 public int getDuration(int i) { 178 return mAnimationState.mDurations[i]; 179 } 180 181 /** 182 * @return True of the animation will play once, false otherwise 183 */ 184 public boolean isOneShot() { 185 return mAnimationState.mOneShot; 186 } 187 188 /** 189 * Sets whether the animation should play once or repeat. 190 * 191 * @param oneShot Pass true if the animation should only play once 192 */ 193 public void setOneShot(boolean oneShot) { 194 mAnimationState.mOneShot = oneShot; 195 } 196 197 /** 198 * Add a frame to the animation 199 * 200 * @param frame The frame to add 201 * @param duration How long in milliseconds the frame should appear 202 */ 203 public void addFrame(Drawable frame, int duration) { 204 mAnimationState.addFrame(frame, duration); 205 if (mCurFrame < 0) { 206 setFrame(0, true, false); 207 } 208 } 209 210 private void nextFrame(boolean unschedule) { 211 int next = mCurFrame+1; 212 final int N = mAnimationState.getChildCount(); 213 if (next >= N) { 214 next = 0; 215 } 216 setFrame(next, unschedule, !mAnimationState.mOneShot || next < (N - 1)); 217 } 218 219 private void setFrame(int frame, boolean unschedule, boolean animate) { 220 if (frame >= mAnimationState.getChildCount()) { 221 return; 222 } 223 mCurFrame = frame; 224 selectDrawable(frame); 225 if (unschedule) { 226 unscheduleSelf(this); 227 } 228 if (animate) { 229 // Unscheduling may have clobbered this value; restore it to record that we're animating 230 mCurFrame = frame; 231 scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]); 232 } 233 } 234 235 @Override 236 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) 237 throws XmlPullParserException, IOException { 238 239 TypedArray a = r.obtainAttributes(attrs, 240 com.android.internal.R.styleable.AnimationDrawable); 241 242 super.inflateWithAttributes(r, parser, a, 243 com.android.internal.R.styleable.AnimationDrawable_visible); 244 245 mAnimationState.setVariablePadding(a.getBoolean( 246 com.android.internal.R.styleable.AnimationDrawable_variablePadding, false)); 247 248 mAnimationState.mOneShot = a.getBoolean( 249 com.android.internal.R.styleable.AnimationDrawable_oneshot, false); 250 251 a.recycle(); 252 253 int type; 254 255 final int innerDepth = parser.getDepth()+1; 256 int depth; 257 while ((type=parser.next()) != XmlPullParser.END_DOCUMENT && 258 ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { 259 if (type != XmlPullParser.START_TAG) { 260 continue; 261 } 262 263 if (depth > innerDepth || !parser.getName().equals("item")) { 264 continue; 265 } 266 267 a = r.obtainAttributes(attrs, com.android.internal.R.styleable.AnimationDrawableItem); 268 int duration = a.getInt( 269 com.android.internal.R.styleable.AnimationDrawableItem_duration, -1); 270 if (duration < 0) { 271 throw new XmlPullParserException( 272 parser.getPositionDescription() 273 + ": <item> tag requires a 'duration' attribute"); 274 } 275 int drawableRes = a.getResourceId( 276 com.android.internal.R.styleable.AnimationDrawableItem_drawable, 0); 277 278 a.recycle(); 279 280 Drawable dr; 281 if (drawableRes != 0) { 282 dr = r.getDrawable(drawableRes); 283 } else { 284 while ((type=parser.next()) == XmlPullParser.TEXT) { 285 // Empty 286 } 287 if (type != XmlPullParser.START_TAG) { 288 throw new XmlPullParserException(parser.getPositionDescription() + 289 ": <item> tag requires a 'drawable' attribute or child tag" + 290 " defining a drawable"); 291 } 292 dr = Drawable.createFromXmlInner(r, parser, attrs); 293 } 294 295 mAnimationState.addFrame(dr, duration); 296 if (dr != null) { 297 dr.setCallback(this); 298 } 299 } 300 301 setFrame(0, true, false); 302 } 303 304 @Override 305 public Drawable mutate() { 306 if (!mMutated && super.mutate() == this) { 307 mAnimationState.mDurations = mAnimationState.mDurations.clone(); 308 mMutated = true; 309 } 310 return this; 311 } 312 313 private final static class AnimationState extends DrawableContainerState { 314 private int[] mDurations; 315 private boolean mOneShot; 316 317 AnimationState(AnimationState orig, AnimationDrawable owner, 318 Resources res) { 319 super(orig, owner, res); 320 321 if (orig != null) { 322 mDurations = orig.mDurations; 323 mOneShot = orig.mOneShot; 324 } else { 325 mDurations = new int[getCapacity()]; 326 mOneShot = true; 327 } 328 } 329 330 @Override 331 public Drawable newDrawable() { 332 return new AnimationDrawable(this, null); 333 } 334 335 @Override 336 public Drawable newDrawable(Resources res) { 337 return new AnimationDrawable(this, res); 338 } 339 340 public void addFrame(Drawable dr, int dur) { 341 // Do not combine the following. The array index must be evaluated before 342 // the array is accessed because super.addChild(dr) has a side effect on mDurations. 343 int pos = super.addChild(dr); 344 mDurations[pos] = dur; 345 } 346 347 @Override 348 public void growArray(int oldSize, int newSize) { 349 super.growArray(oldSize, newSize); 350 int[] newDurations = new int[newSize]; 351 System.arraycopy(mDurations, 0, newDurations, 0, oldSize); 352 mDurations = newDurations; 353 } 354 } 355 356 private AnimationDrawable(AnimationState state, Resources res) { 357 AnimationState as = new AnimationState(state, this, res); 358 mAnimationState = as; 359 setConstantState(as); 360 if (state != null) { 361 setFrame(0, true, false); 362 } 363 } 364 } 365 366