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