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