AnimationDrawable.java revision 9366ce81dbaa1c954911ef1c8d5791bb4a69d89c
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 #start()} to run 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 * <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 */
81public 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.getChildren()[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            scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
230        }
231    }
232
233    @Override
234    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
235            throws XmlPullParserException, IOException {
236
237        TypedArray a = r.obtainAttributes(attrs,
238                com.android.internal.R.styleable.AnimationDrawable);
239
240        super.inflateWithAttributes(r, parser, a,
241                com.android.internal.R.styleable.AnimationDrawable_visible);
242
243        mAnimationState.setVariablePadding(a.getBoolean(
244                com.android.internal.R.styleable.AnimationDrawable_variablePadding, false));
245
246        mAnimationState.mOneShot = a.getBoolean(
247                com.android.internal.R.styleable.AnimationDrawable_oneshot, false);
248
249        a.recycle();
250
251        int type;
252
253        final int innerDepth = parser.getDepth()+1;
254        int depth;
255        while ((type=parser.next()) != XmlPullParser.END_DOCUMENT &&
256                ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
257            if (type != XmlPullParser.START_TAG) {
258                continue;
259            }
260
261            if (depth > innerDepth || !parser.getName().equals("item")) {
262                continue;
263            }
264
265            a = r.obtainAttributes(attrs, com.android.internal.R.styleable.AnimationDrawableItem);
266            int duration = a.getInt(
267                    com.android.internal.R.styleable.AnimationDrawableItem_duration, -1);
268            if (duration < 0) {
269                throw new XmlPullParserException(
270                        parser.getPositionDescription()
271                        + ": <item> tag requires a 'duration' attribute");
272            }
273            int drawableRes = a.getResourceId(
274                    com.android.internal.R.styleable.AnimationDrawableItem_drawable, 0);
275
276            a.recycle();
277
278            Drawable dr;
279            if (drawableRes != 0) {
280                dr = r.getDrawable(drawableRes);
281            } else {
282                while ((type=parser.next()) == XmlPullParser.TEXT) {
283                    // Empty
284                }
285                if (type != XmlPullParser.START_TAG) {
286                    throw new XmlPullParserException(parser.getPositionDescription() +
287                            ": <item> tag requires a 'drawable' attribute or child tag" +
288                            " defining a drawable");
289                }
290                dr = Drawable.createFromXmlInner(r, parser, attrs);
291            }
292
293            mAnimationState.addFrame(dr, duration);
294            if (dr != null) {
295                dr.setCallback(this);
296            }
297        }
298
299        setFrame(0, true, false);
300    }
301
302    @Override
303    public Drawable mutate() {
304        if (!mMutated && super.mutate() == this) {
305            mAnimationState.mDurations = mAnimationState.mDurations.clone();
306            mMutated = true;
307        }
308        return this;
309    }
310
311    private final static class AnimationState extends DrawableContainerState {
312        private int[] mDurations;
313        private boolean mOneShot;
314
315        AnimationState(AnimationState orig, AnimationDrawable owner,
316                Resources res) {
317            super(orig, owner, res);
318
319            if (orig != null) {
320                mDurations = orig.mDurations;
321                mOneShot = orig.mOneShot;
322            } else {
323                mDurations = new int[getChildren().length];
324                mOneShot = true;
325            }
326        }
327
328        @Override
329        public Drawable newDrawable() {
330            return new AnimationDrawable(this, null);
331        }
332
333        @Override
334        public Drawable newDrawable(Resources res) {
335            return new AnimationDrawable(this, res);
336        }
337
338        public void addFrame(Drawable dr, int dur) {
339            // Do not combine the following. The array index must be evaluated before
340            // the array is accessed because super.addChild(dr) has a side effect on mDurations.
341            int pos = super.addChild(dr);
342            mDurations[pos] = dur;
343        }
344
345        @Override
346        public void growArray(int oldSize, int newSize) {
347            super.growArray(oldSize, newSize);
348            int[] newDurations = new int[newSize];
349            System.arraycopy(mDurations, 0, newDurations, 0, oldSize);
350            mDurations = newDurations;
351        }
352    }
353
354    private AnimationDrawable(AnimationState state, Resources res) {
355        AnimationState as = new AnimationState(state, this, res);
356        mAnimationState = as;
357        setConstantState(as);
358        if (state != null) {
359            setFrame(0, true, false);
360        }
361    }
362}
363
364