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, ranging from 0 to {@link #mAnimationState#getChildCount() - 1} */
91    private int mCurFrame = 0;
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 || !mRunning ||
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            // Start from 0th frame.
155            setFrame(0, false, mAnimationState.getChildCount() > 1
156                    || !mAnimationState.mOneShot);
157        }
158    }
159
160    /**
161     * Stops the animation. This method has no effect if the animation is not
162     * running.
163     *
164     * @see #isRunning()
165     * @see #start()
166     */
167    @Override
168    public void stop() {
169        mAnimating = false;
170
171        if (isRunning()) {
172            unscheduleSelf(this);
173        }
174    }
175
176    /**
177     * Indicates whether the animation is currently running or not.
178     *
179     * @return true if the animation is running, false otherwise
180     */
181    @Override
182    public boolean isRunning() {
183        return mRunning;
184    }
185
186    /**
187     * This method exists for implementation purpose only and should not be
188     * called directly. Invoke {@link #start()} instead.
189     *
190     * @see #start()
191     */
192    @Override
193    public void run() {
194        nextFrame(false);
195    }
196
197    @Override
198    public void unscheduleSelf(Runnable what) {
199        mCurFrame = 0;
200        mRunning = false;
201        super.unscheduleSelf(what);
202    }
203
204    /**
205     * @return The number of frames in the animation
206     */
207    public int getNumberOfFrames() {
208        return mAnimationState.getChildCount();
209    }
210
211    /**
212     * @return The Drawable at the specified frame index
213     */
214    public Drawable getFrame(int index) {
215        return mAnimationState.getChild(index);
216    }
217
218    /**
219     * @return The duration in milliseconds of the frame at the
220     *         specified index
221     */
222    public int getDuration(int i) {
223        return mAnimationState.mDurations[i];
224    }
225
226    /**
227     * @return True of the animation will play once, false otherwise
228     */
229    public boolean isOneShot() {
230        return mAnimationState.mOneShot;
231    }
232
233    /**
234     * Sets whether the animation should play once or repeat.
235     *
236     * @param oneShot Pass true if the animation should only play once
237     */
238    public void setOneShot(boolean oneShot) {
239        mAnimationState.mOneShot = oneShot;
240    }
241
242    /**
243     * Adds a frame to the animation
244     *
245     * @param frame The frame to add
246     * @param duration How long in milliseconds the frame should appear
247     */
248    public void addFrame(@NonNull Drawable frame, int duration) {
249        mAnimationState.addFrame(frame, duration);
250        if (!mRunning) {
251            setFrame(0, true, false);
252        }
253    }
254
255    private void nextFrame(boolean unschedule) {
256        int nextFrame = mCurFrame + 1;
257        final int numFrames = mAnimationState.getChildCount();
258        final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1);
259
260        // Loop if necessary. One-shot animations should never hit this case.
261        if (!mAnimationState.mOneShot && nextFrame >= numFrames) {
262            nextFrame = 0;
263        }
264
265        setFrame(nextFrame, unschedule, !isLastFrame);
266    }
267
268    private void setFrame(int frame, boolean unschedule, boolean animate) {
269        if (frame >= mAnimationState.getChildCount()) {
270            return;
271        }
272        mAnimating = animate;
273        mCurFrame = frame;
274        selectDrawable(frame);
275        if (unschedule || animate) {
276            unscheduleSelf(this);
277        }
278        if (animate) {
279            // Unscheduling may have clobbered these values; restore them
280            mCurFrame = frame;
281            mRunning = true;
282            scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
283        }
284    }
285
286    @Override
287    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
288            throws XmlPullParserException, IOException {
289        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable);
290        super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible);
291        updateStateFromTypedArray(a);
292        a.recycle();
293
294        inflateChildElements(r, parser, attrs, theme);
295
296        setFrame(0, true, false);
297    }
298
299    private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
300            Theme theme) throws XmlPullParserException, IOException {
301        int type;
302
303        final int innerDepth = parser.getDepth()+1;
304        int depth;
305        while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
306                && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
307            if (type != XmlPullParser.START_TAG) {
308                continue;
309            }
310
311            if (depth > innerDepth || !parser.getName().equals("item")) {
312                continue;
313            }
314
315            final TypedArray a = obtainAttributes(r, theme, attrs,
316                    R.styleable.AnimationDrawableItem);
317
318            final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1);
319            if (duration < 0) {
320                throw new XmlPullParserException(parser.getPositionDescription()
321                        + ": <item> tag requires a 'duration' attribute");
322            }
323
324            Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable);
325
326            a.recycle();
327
328            if (dr == null) {
329                while ((type=parser.next()) == XmlPullParser.TEXT) {
330                    // Empty
331                }
332                if (type != XmlPullParser.START_TAG) {
333                    throw new XmlPullParserException(parser.getPositionDescription()
334                            + ": <item> tag requires a 'drawable' attribute or child tag"
335                            + " defining a drawable");
336                }
337                dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
338            }
339
340            mAnimationState.addFrame(dr, duration);
341            if (dr != null) {
342                dr.setCallback(this);
343            }
344        }
345    }
346
347    private void updateStateFromTypedArray(TypedArray a) {
348        mAnimationState.mVariablePadding = a.getBoolean(
349                R.styleable.AnimationDrawable_variablePadding, mAnimationState.mVariablePadding);
350
351        mAnimationState.mOneShot = a.getBoolean(
352                R.styleable.AnimationDrawable_oneshot, mAnimationState.mOneShot);
353    }
354
355    @Override
356    @NonNull
357    public Drawable mutate() {
358        if (!mMutated && super.mutate() == this) {
359            mAnimationState.mutate();
360            mMutated = true;
361        }
362        return this;
363    }
364
365    @Override
366    AnimationState cloneConstantState() {
367        return new AnimationState(mAnimationState, this, null);
368    }
369
370    /**
371     * @hide
372     */
373    public void clearMutated() {
374        super.clearMutated();
375        mMutated = false;
376    }
377
378    private final static class AnimationState extends DrawableContainerState {
379        private int[] mDurations;
380        private boolean mOneShot = false;
381
382        AnimationState(AnimationState orig, AnimationDrawable owner, Resources res) {
383            super(orig, owner, res);
384
385            if (orig != null) {
386                mDurations = orig.mDurations;
387                mOneShot = orig.mOneShot;
388            } else {
389                mDurations = new int[getCapacity()];
390                mOneShot = false;
391            }
392        }
393
394        private void mutate() {
395            mDurations = mDurations.clone();
396        }
397
398        @Override
399        public Drawable newDrawable() {
400            return new AnimationDrawable(this, null);
401        }
402
403        @Override
404        public Drawable newDrawable(Resources res) {
405            return new AnimationDrawable(this, res);
406        }
407
408        public void addFrame(Drawable dr, int dur) {
409            // Do not combine the following. The array index must be evaluated before
410            // the array is accessed because super.addChild(dr) has a side effect on mDurations.
411            int pos = super.addChild(dr);
412            mDurations[pos] = dur;
413        }
414
415        @Override
416        public void growArray(int oldSize, int newSize) {
417            super.growArray(oldSize, newSize);
418            int[] newDurations = new int[newSize];
419            System.arraycopy(mDurations, 0, newDurations, 0, oldSize);
420            mDurations = newDurations;
421        }
422    }
423
424    @Override
425    protected void setConstantState(@NonNull DrawableContainerState state) {
426        super.setConstantState(state);
427
428        if (state instanceof AnimationState) {
429            mAnimationState = (AnimationState) state;
430        }
431    }
432
433    private AnimationDrawable(AnimationState state, Resources res) {
434        final AnimationState as = new AnimationState(state, this, res);
435        setConstantState(as);
436        if (state != null) {
437            setFrame(0, true, false);
438        }
439    }
440}
441
442