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>&lt;animation-list></code> element,
41 * and a series of nested <code>&lt;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>&lt;!-- Animation frames are wheel0.png -- wheel5.png files inside the
46 * res/drawable/ folder --&gt;
47 * &lt;animation-list android:id=&quot;@+id/selected&quot; android:oneshot=&quot;false&quot;&gt;
48 *    &lt;item android:drawable=&quot;@drawable/wheel0&quot; android:duration=&quot;50&quot; /&gt;
49 *    &lt;item android:drawable=&quot;@drawable/wheel1&quot; android:duration=&quot;50&quot; /&gt;
50 *    &lt;item android:drawable=&quot;@drawable/wheel2&quot; android:duration=&quot;50&quot; /&gt;
51 *    &lt;item android:drawable=&quot;@drawable/wheel3&quot; android:duration=&quot;50&quot; /&gt;
52 *    &lt;item android:drawable=&quot;@drawable/wheel4&quot; android:duration=&quot;50&quot; /&gt;
53 *    &lt;item android:drawable=&quot;@drawable/wheel5&quot; android:duration=&quot;50&quot; /&gt;
54 * &lt;/animation-list&gt;</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