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