AnimationDrawable.java revision b798689749c64baba81f02e10cf2157c747d6b46
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.os.SystemClock;
27import 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 #run()} to start the animation.
37 * <p>
38 * An AnimationDrawable defined in XML consists of a single <code>&lt;animation-list></code> element,
39 * and a series of nested <code>&lt;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>&lt;!-- Animation frames are wheel0.png -- wheel5.png files inside the
44 * res/drawable/ folder --&gt;
45 * &lt;animation-list android:id=&quot;selected&quot; android:oneshot=&quot;false&quot;&gt;
46 *    &lt;item android:drawable=&quot;@drawable/wheel0&quot; android:duration=&quot;50&quot; /&gt;
47 *    &lt;item android:drawable=&quot;@drawable/wheel1&quot; android:duration=&quot;50&quot; /&gt;
48 *    &lt;item android:drawable=&quot;@drawable/wheel2&quot; android:duration=&quot;50&quot; /&gt;
49 *    &lt;item android:drawable=&quot;@drawable/wheel3&quot; android:duration=&quot;50&quot; /&gt;
50 *    &lt;item android:drawable=&quot;@drawable/wheel4&quot; android:duration=&quot;50&quot; /&gt;
51 *    &lt;item android:drawable=&quot;@drawable/wheel5&quot; android:duration=&quot;50&quot; /&gt;
52 * &lt;/animation-list&gt;</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 * @attr ref android.R.styleable#AnimationDrawable_visible
69 * @attr ref android.R.styleable#AnimationDrawable_variablePadding
70 * @attr ref android.R.styleable#AnimationDrawable_oneshot
71 * @attr ref android.R.styleable#AnimationDrawableItem_duration
72 * @attr ref android.R.styleable#AnimationDrawableItem_drawable
73 */
74public class AnimationDrawable extends DrawableContainer implements Runnable {
75    private final AnimationState mAnimationState;
76    private int mCurFrame = -1;
77
78    public AnimationDrawable() {
79        this(null);
80    }
81
82    @Override
83    public boolean setVisible(boolean visible, boolean restart) {
84        boolean changed = super.setVisible(visible, restart);
85        if (visible) {
86            if (changed || restart) {
87                setFrame(0, true, true);
88            }
89        } else {
90            unscheduleSelf(this);
91        }
92        return changed;
93    }
94
95    /**
96     * <p>Starts the animation, looping if necessary. This method has no effect
97     * if the animation is running.</p>
98     *
99     * @see #isRunning()
100     * @see #stop()
101     */
102    public void start() {
103        if (!isRunning()) {
104            run();
105        }
106    }
107
108    /**
109     * <p>Stops the animation. This method has no effect if the animation is
110     * not running.</p>
111     *
112     * @see #isRunning()
113     * @see #start()
114     */
115    public void stop() {
116        if (isRunning()) {
117            unscheduleSelf(this);
118        }
119    }
120
121    /**
122     * <p>Indicates whether the animation is currently running or not.</p>
123     *
124     * @return true if the animation is running, false otherwise
125     */
126    public boolean isRunning() {
127        return mCurFrame > -1;
128    }
129
130    /**
131     * <p>This method exists for implementation purpose only and should not be
132     * called directly. Invoke {@link #start()} instead.</p>
133     *
134     * @see #start()
135     */
136    public void run() {
137        nextFrame(false);
138    }
139
140    @Override
141    public void unscheduleSelf(Runnable what) {
142        mCurFrame = -1;
143        super.unscheduleSelf(what);
144    }
145
146    /**
147     * @return The number of frames in the animation
148     */
149    public int getNumberOfFrames() {
150        return mAnimationState.getChildCount();
151    }
152
153    /**
154     * @return The Drawable at the specified frame index
155     */
156    public Drawable getFrame(int index) {
157        return mAnimationState.getChildren()[index];
158    }
159
160    /**
161     * @return The duration in milliseconds of the frame at the
162     * specified index
163     */
164    public int getDuration(int i) {
165        return mAnimationState.mDurations[i];
166    }
167
168    /**
169     * @return True of the animation will play once, false otherwise
170     */
171    public boolean isOneShot() {
172        return mAnimationState.mOneShot;
173    }
174
175    /**
176     * Sets whether the animation should play once or repeat.
177     *
178     * @param oneShot Pass true if the animation should only play once
179     */
180    public void setOneShot(boolean oneShot) {
181        mAnimationState.mOneShot = oneShot;
182    }
183
184    /**
185     * Add a frame to the animation
186     *
187     * @param frame The frame to add
188     * @param duration How long in milliseconds the frame should appear
189     */
190    public void addFrame(Drawable frame, int duration) {
191        mAnimationState.addFrame(frame, duration);
192    }
193
194    private void nextFrame(boolean unschedule) {
195        int next = mCurFrame+1;
196        final int N = mAnimationState.getChildCount();
197        if (next >= N) {
198            next = 0;
199        }
200        setFrame(next, unschedule, !mAnimationState.mOneShot || next < (N - 1));
201    }
202
203    private void setFrame(int frame, boolean unschedule, boolean animate) {
204        if (frame >= mAnimationState.getChildCount()) {
205            return;
206        }
207        mCurFrame = frame;
208        selectDrawable(frame);
209        if (unschedule) {
210            unscheduleSelf(this);
211        }
212        if (animate) {
213            scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
214        }
215    }
216
217    @Override
218    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
219            throws XmlPullParserException, IOException {
220
221        TypedArray a = r.obtainAttributes(attrs,
222                com.android.internal.R.styleable.AnimationDrawable);
223
224        super.inflateWithAttributes(r, parser, a,
225                com.android.internal.R.styleable.AnimationDrawable_visible);
226
227        mAnimationState.setVariablePadding(a.getBoolean(
228                com.android.internal.R.styleable.AnimationDrawable_variablePadding, false));
229
230        mAnimationState.mOneShot = a.getBoolean(
231                com.android.internal.R.styleable.AnimationDrawable_oneshot, false);
232
233        a.recycle();
234
235        int type;
236
237        final int innerDepth = parser.getDepth()+1;
238        int depth;
239        while ((type=parser.next()) != XmlPullParser.END_DOCUMENT &&
240                ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
241            if (type != XmlPullParser.START_TAG) {
242                continue;
243            }
244
245            if (depth > innerDepth || !parser.getName().equals("item")) {
246                continue;
247            }
248
249            a = r.obtainAttributes(attrs, com.android.internal.R.styleable.AnimationDrawableItem);
250            int duration = a.getInt(
251                    com.android.internal.R.styleable.AnimationDrawableItem_duration, -1);
252            if (duration < 0) {
253                throw new XmlPullParserException(
254                        parser.getPositionDescription()
255                        + ": <item> tag requires a 'duration' attribute");
256            }
257            int drawableRes = a.getResourceId(
258                    com.android.internal.R.styleable.AnimationDrawableItem_drawable, 0);
259
260            a.recycle();
261
262            Drawable dr;
263            if (drawableRes != 0) {
264                dr = r.getDrawable(drawableRes);
265            } else {
266                while ((type=parser.next()) == XmlPullParser.TEXT) {
267                    // Empty
268                }
269                if (type != XmlPullParser.START_TAG) {
270                    throw new XmlPullParserException(parser.getPositionDescription() +
271                            ": <item> tag requires a 'drawable' attribute or child tag" +
272                            " defining a drawable");
273                }
274                dr = Drawable.createFromXmlInner(r, parser, attrs);
275            }
276
277            mAnimationState.addFrame(dr, duration);
278            if (dr != null) {
279                dr.setCallback(this);
280            }
281        }
282
283        setFrame(0, true, false);
284    }
285
286    private final static class AnimationState extends DrawableContainerState {
287        private int[] mDurations;
288        private boolean mOneShot;
289
290        AnimationState(AnimationState orig, AnimationDrawable owner) {
291            super(orig, owner);
292
293            if (orig != null) {
294                mDurations = orig.mDurations;
295                mOneShot = orig.mOneShot;
296            } else {
297                mDurations = new int[getChildren().length];
298                mOneShot = true;
299            }
300        }
301
302        @Override
303        public Drawable newDrawable() {
304            return new AnimationDrawable(this);
305        }
306
307        public void addFrame(Drawable dr, int dur) {
308            // Do not combine the following. The array index must be evaluated before
309            // the array is accessed because super.addChild(dr) has a side effect on mDurations.
310            int pos = super.addChild(dr);
311            mDurations[pos] = dur;
312        }
313
314        @Override
315        public void growArray(int oldSize, int newSize) {
316            super.growArray(oldSize, newSize);
317            int[] newDurations = new int[newSize];
318            System.arraycopy(mDurations, 0, newDurations, 0, oldSize);
319            mDurations = newDurations;
320        }
321    }
322
323    private AnimationDrawable(AnimationState state) {
324        AnimationState as = new AnimationState(state, this);
325        mAnimationState = as;
326        setConstantState(as);
327        if (state != null) {
328            setFrame(0, true, false);
329        }
330    }
331}
332
333