AnimationDrawable.java revision 39e33621a725bcdaa21a723866e53c6ea3356169
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 * 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;@+id/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
84    /** The current frame, may be -1 when not animating. */
85    private int mCurFrame = -1;
86
87    /** Whether the drawable has an animation callback posted. */
88    private boolean mRunning;
89
90    /** Whether the drawable should animate when visible. */
91    private boolean mAnimating;
92
93    private boolean mMutated;
94
95    public AnimationDrawable() {
96        this(null, null);
97    }
98
99    /**
100     * Sets whether this AnimationDrawable is visible.
101     * <p>
102     * When the drawable becomes invisible, it will pause its animation. A
103     * subsequent change to visible with <code>restart</code> set to true will
104     * restart the animation from the first frame. If <code>restart</code> is
105     * false, the animation will resume from the most recent frame.
106     *
107     * @param visible true if visible, false otherwise
108     * @param restart when visible, true to force the animation to restart
109     *                from the first frame
110     * @return true if the new visibility is different than its previous state
111     */
112    @Override
113    public boolean setVisible(boolean visible, boolean restart) {
114        final boolean changed = super.setVisible(visible, restart);
115        if (visible) {
116            if (restart || changed) {
117                boolean startFromZero = restart || mCurFrame < 0 ||
118                        mCurFrame >= mAnimationState.getChildCount();
119                setFrame(startFromZero ? 0 : mCurFrame, true, mAnimating);
120            }
121        } else {
122            unscheduleSelf(this);
123        }
124        return changed;
125    }
126
127    /**
128     * <p>Starts the animation, looping if necessary. This method has no effect
129     * if the animation is running. Do not call this in the {@link android.app.Activity#onCreate}
130     * method of your activity, because the {@link android.graphics.drawable.AnimationDrawable} is
131     * not yet fully attached to the window. If you want to play
132     * the animation immediately, without requiring interaction, then you might want to call it
133     * from the {@link android.app.Activity#onWindowFocusChanged} method in your activity,
134     * which will get called when Android brings your window into focus.</p>
135     *
136     * @see #isRunning()
137     * @see #stop()
138     */
139    @Override
140    public void start() {
141        mAnimating = true;
142
143        if (!isRunning()) {
144            run();
145        }
146    }
147
148    /**
149     * <p>Stops the animation. This method has no effect if the animation is
150     * not running.</p>
151     *
152     * @see #isRunning()
153     * @see #start()
154     */
155    @Override
156    public void stop() {
157        mAnimating = false;
158
159        if (isRunning()) {
160            unscheduleSelf(this);
161        }
162    }
163
164    /**
165     * <p>Indicates whether the animation is currently running or not.</p>
166     *
167     * @return true if the animation is running, false otherwise
168     */
169    @Override
170    public boolean isRunning() {
171        return mRunning;
172    }
173
174    /**
175     * <p>This method exists for implementation purpose only and should not be
176     * called directly. Invoke {@link #start()} instead.</p>
177     *
178     * @see #start()
179     */
180    @Override
181    public void run() {
182        nextFrame(false);
183    }
184
185    @Override
186    public void unscheduleSelf(Runnable what) {
187        mCurFrame = -1;
188        mRunning = false;
189        super.unscheduleSelf(what);
190    }
191
192    /**
193     * @return The number of frames in the animation
194     */
195    public int getNumberOfFrames() {
196        return mAnimationState.getChildCount();
197    }
198
199    /**
200     * @return The Drawable at the specified frame index
201     */
202    public Drawable getFrame(int index) {
203        return mAnimationState.getChild(index);
204    }
205
206    /**
207     * @return The duration in milliseconds of the frame at the
208     * specified index
209     */
210    public int getDuration(int i) {
211        return mAnimationState.mDurations[i];
212    }
213
214    /**
215     * @return True of the animation will play once, false otherwise
216     */
217    public boolean isOneShot() {
218        return mAnimationState.mOneShot;
219    }
220
221    /**
222     * Sets whether the animation should play once or repeat.
223     *
224     * @param oneShot Pass true if the animation should only play once
225     */
226    public void setOneShot(boolean oneShot) {
227        mAnimationState.mOneShot = oneShot;
228    }
229
230    /**
231     * Add a frame to the animation
232     *
233     * @param frame The frame to add
234     * @param duration How long in milliseconds the frame should appear
235     */
236    public void addFrame(Drawable frame, int duration) {
237        mAnimationState.addFrame(frame, duration);
238        if (mCurFrame < 0) {
239            setFrame(0, true, false);
240        }
241    }
242
243    private void nextFrame(boolean unschedule) {
244        int next = mCurFrame+1;
245        final int N = mAnimationState.getChildCount();
246        if (next >= N) {
247            next = 0;
248        }
249
250        setFrame(next, unschedule, !mAnimationState.mOneShot || next < (N - 1));
251    }
252
253    private void setFrame(int frame, boolean unschedule, boolean animate) {
254        if (frame >= mAnimationState.getChildCount()) {
255            return;
256        }
257        mAnimating = animate;
258        mCurFrame = frame;
259        selectDrawable(frame);
260        if (unschedule || animate) {
261            unscheduleSelf(this);
262        }
263        if (animate) {
264            // Unscheduling may have clobbered these values; restore them
265            mCurFrame = frame;
266            mRunning = true;
267            scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
268        }
269    }
270
271    @Override
272    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
273            throws XmlPullParserException, IOException {
274
275        TypedArray a = obtainAttributes(r, theme, attrs,
276                com.android.internal.R.styleable.AnimationDrawable);
277
278        super.inflateWithAttributes(r, parser, a,
279                com.android.internal.R.styleable.AnimationDrawable_visible);
280
281        mAnimationState.setVariablePadding(a.getBoolean(
282                com.android.internal.R.styleable.AnimationDrawable_variablePadding, false));
283
284        mAnimationState.mOneShot = a.getBoolean(
285                com.android.internal.R.styleable.AnimationDrawable_oneshot, false);
286
287        a.recycle();
288
289        int type;
290
291        final int innerDepth = parser.getDepth()+1;
292        int depth;
293        while ((type=parser.next()) != XmlPullParser.END_DOCUMENT &&
294                ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
295            if (type != XmlPullParser.START_TAG) {
296                continue;
297            }
298
299            if (depth > innerDepth || !parser.getName().equals("item")) {
300                continue;
301            }
302
303            a = obtainAttributes(
304                    r, theme, attrs, com.android.internal.R.styleable.AnimationDrawableItem);
305            int duration = a.getInt(
306                    com.android.internal.R.styleable.AnimationDrawableItem_duration, -1);
307            if (duration < 0) {
308                throw new XmlPullParserException(
309                        parser.getPositionDescription()
310                        + ": <item> tag requires a 'duration' attribute");
311            }
312            int drawableRes = a.getResourceId(
313                    com.android.internal.R.styleable.AnimationDrawableItem_drawable, 0);
314
315            a.recycle();
316
317            Drawable dr;
318            if (drawableRes != 0) {
319                dr = r.getDrawable(drawableRes, theme);
320            } else {
321                while ((type=parser.next()) == XmlPullParser.TEXT) {
322                    // Empty
323                }
324                if (type != XmlPullParser.START_TAG) {
325                    throw new XmlPullParserException(parser.getPositionDescription() +
326                            ": <item> tag requires a 'drawable' attribute or child tag" +
327                            " defining a drawable");
328                }
329                dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
330            }
331
332            mAnimationState.addFrame(dr, duration);
333            if (dr != null) {
334                dr.setCallback(this);
335            }
336        }
337
338        setFrame(0, true, false);
339    }
340
341    @Override
342    public Drawable mutate() {
343        if (!mMutated && super.mutate() == this) {
344            mAnimationState.mDurations = mAnimationState.mDurations.clone();
345            mMutated = true;
346        }
347        return this;
348    }
349
350    private final static class AnimationState extends DrawableContainerState {
351        private int[] mDurations;
352        private boolean mOneShot;
353
354        AnimationState(AnimationState orig, AnimationDrawable owner,
355                Resources res) {
356            super(orig, owner, res);
357
358            if (orig != null) {
359                mDurations = orig.mDurations;
360                mOneShot = orig.mOneShot;
361            } else {
362                mDurations = new int[getCapacity()];
363                mOneShot = true;
364            }
365        }
366
367        @Override
368        public Drawable newDrawable() {
369            return new AnimationDrawable(this, null);
370        }
371
372        @Override
373        public Drawable newDrawable(Resources res) {
374            return new AnimationDrawable(this, res);
375        }
376
377        public void addFrame(Drawable dr, int dur) {
378            // Do not combine the following. The array index must be evaluated before
379            // the array is accessed because super.addChild(dr) has a side effect on mDurations.
380            int pos = super.addChild(dr);
381            mDurations[pos] = dur;
382        }
383
384        @Override
385        public void growArray(int oldSize, int newSize) {
386            super.growArray(oldSize, newSize);
387            int[] newDurations = new int[newSize];
388            System.arraycopy(mDurations, 0, newDurations, 0, oldSize);
389            mDurations = newDurations;
390        }
391    }
392
393    private AnimationDrawable(AnimationState state, Resources res) {
394        AnimationState as = new AnimationState(state, this, res);
395        mAnimationState = as;
396        setConstantState(as);
397        if (state != null) {
398            setFrame(0, true, false);
399        }
400    }
401}
402
403