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