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