AnimationDrawable.java revision 56ef127df75164243a21df10771e7c77c8ef2e24
1192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta/*
2192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * Copyright (C) 2006 The Android Open Source Project
3192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *
4192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * Licensed under the Apache License, Version 2.0 (the "License");
5192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * you may not use this file except in compliance with the License.
6192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * You may obtain a copy of the License at
7192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *
8192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *      http://www.apache.org/licenses/LICENSE-2.0
9192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *
10192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * Unless required by applicable law or agreed to in writing, software
11192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * distributed under the License is distributed on an "AS IS" BASIS,
12192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * See the License for the specific language governing permissions and
14192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * limitations under the License.
15192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta */
16192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
17192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptapackage android.graphics.drawable;
18192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
19192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport com.android.internal.R;
20192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
21192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport java.io.IOException;
22192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
23192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport org.xmlpull.v1.XmlPullParser;
24192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport org.xmlpull.v1.XmlPullParserException;
25192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
26192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport android.annotation.NonNull;
27192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport android.content.res.Resources;
28192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport android.content.res.TypedArray;
29192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport android.content.res.Resources.Theme;
30192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport android.os.SystemClock;
31192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptaimport android.util.AttributeSet;
32192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
33192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta/**
34192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * An object used to create frame-by-frame animations, defined by a series of
35192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * Drawable objects, which can be used as a View object's background.
36192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <p>
37192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * The simplest way to create a frame-by-frame animation is to define the
38192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * animation in an XML file, placed in the res/drawable/ folder, and set it as
39192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * the background to a View object. Then, call {@link #start()} to run the
40192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * animation.
41192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <p>
42192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * An AnimationDrawable defined in XML consists of a single
43192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * {@code &lt;animation-list&gt;} element and a series of nested
44192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * {@code &lt;item&gt;} tags. Each item defines a frame of the animation. See
45192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * the example below.
46192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <p>
47192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * spin_animation.xml file in res/drawable/ folder:
48192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <pre>
49192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * &lt;!-- Animation frames are wheel0.png through wheel5.png
50192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *     files inside the res/drawable/ folder --&gt;
51192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * &lt;animation-list android:id=&quot;@+id/selected&quot; android:oneshot=&quot;false&quot;&gt;
52192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *    &lt;item android:drawable=&quot;@drawable/wheel0&quot; android:duration=&quot;50&quot; /&gt;
53192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *    &lt;item android:drawable=&quot;@drawable/wheel1&quot; android:duration=&quot;50&quot; /&gt;
54192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *    &lt;item android:drawable=&quot;@drawable/wheel2&quot; android:duration=&quot;50&quot; /&gt;
55192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *    &lt;item android:drawable=&quot;@drawable/wheel3&quot; android:duration=&quot;50&quot; /&gt;
56192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *    &lt;item android:drawable=&quot;@drawable/wheel4&quot; android:duration=&quot;50&quot; /&gt;
57192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *    &lt;item android:drawable=&quot;@drawable/wheel5&quot; android:duration=&quot;50&quot; /&gt;
58192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * &lt;/animation-list&gt;</pre>
59192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <p>
60192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * Here is the code to load and play this animation.
61192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <pre>
62192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * // Load the ImageView that will host the animation and
63192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * // set its background to our AnimationDrawable XML resource.
64192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * ImageView img = (ImageView)findViewById(R.id.spinning_wheel_image);
65192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * img.setBackgroundResource(R.drawable.spin_animation);
66192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *
67192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * // Get the background, which has been compiled to an AnimationDrawable object.
68192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * AnimationDrawable frameAnimation = (AnimationDrawable) img.getBackground();
69192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *
70192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * // Start the animation (looped playback by default).
71192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * frameAnimation.start();
72192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * </pre>
73192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *
74192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <div class="special reference">
75192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <h3>Developer Guides</h3>
76192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <p>For more information about animating with {@code AnimationDrawable}, read the
77192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * <a href="{@docRoot}guide/topics/graphics/drawable-animation.html">Drawable Animation</a>
78192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * developer guide.</p>
79192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * </div>
80192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta *
81192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * @attr ref android.R.styleable#AnimationDrawable_visible
82192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * @attr ref android.R.styleable#AnimationDrawable_variablePadding
83192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * @attr ref android.R.styleable#AnimationDrawable_oneshot
84192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * @attr ref android.R.styleable#AnimationDrawableItem_duration
85192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta * @attr ref android.R.styleable#AnimationDrawableItem_drawable
86192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta */
87192d793d2586b620027edd5b45ff4c72a86cc7beHemant Guptapublic class AnimationDrawable extends DrawableContainer implements Runnable, Animatable {
88192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    private AnimationState mAnimationState;
89192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
90192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    /** The current frame, ranging from 0 to {@link #mAnimationState#getChildCount() - 1} */
91192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    private int mCurFrame = 0;
92192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
93192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    /** Whether the drawable has an animation callback posted. */
94192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    private boolean mRunning;
95192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
96192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    /** Whether the drawable should animate when visible. */
97192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    private boolean mAnimating;
98192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
99192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    private boolean mMutated;
100192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
101192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    public AnimationDrawable() {
102192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta        this(null, null);
103192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    }
104192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta
105192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta    /**
106192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta     * Sets whether this AnimationDrawable is visible.
107192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta     * <p>
108192d793d2586b620027edd5b45ff4c72a86cc7beHemant Gupta     * 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            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 = 0;
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 (!mRunning) {
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        }
276        if (animate) {
277            // Unscheduling may have clobbered these values; restore them
278            mCurFrame = frame;
279            mRunning = true;
280            scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
281        }
282    }
283
284    @Override
285    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
286            throws XmlPullParserException, IOException {
287        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable);
288        super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible);
289        updateStateFromTypedArray(a);
290        a.recycle();
291
292        inflateChildElements(r, parser, attrs, theme);
293
294        setFrame(0, true, false);
295    }
296
297    private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
298            Theme theme) throws XmlPullParserException, IOException {
299        int type;
300
301        final int innerDepth = parser.getDepth()+1;
302        int depth;
303        while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
304                && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
305            if (type != XmlPullParser.START_TAG) {
306                continue;
307            }
308
309            if (depth > innerDepth || !parser.getName().equals("item")) {
310                continue;
311            }
312
313            final TypedArray a = obtainAttributes(r, theme, attrs,
314                    R.styleable.AnimationDrawableItem);
315
316            final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1);
317            if (duration < 0) {
318                throw new XmlPullParserException(parser.getPositionDescription()
319                        + ": <item> tag requires a 'duration' attribute");
320            }
321
322            Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable);
323
324            a.recycle();
325
326            if (dr == null) {
327                while ((type=parser.next()) == XmlPullParser.TEXT) {
328                    // Empty
329                }
330                if (type != XmlPullParser.START_TAG) {
331                    throw new XmlPullParserException(parser.getPositionDescription()
332                            + ": <item> tag requires a 'drawable' attribute or child tag"
333                            + " defining a drawable");
334                }
335                dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
336            }
337
338            mAnimationState.addFrame(dr, duration);
339            if (dr != null) {
340                dr.setCallback(this);
341            }
342        }
343    }
344
345    private void updateStateFromTypedArray(TypedArray a) {
346        mAnimationState.mVariablePadding = a.getBoolean(
347                R.styleable.AnimationDrawable_variablePadding, mAnimationState.mVariablePadding);
348
349        mAnimationState.mOneShot = a.getBoolean(
350                R.styleable.AnimationDrawable_oneshot, mAnimationState.mOneShot);
351    }
352
353    @Override
354    @NonNull
355    public Drawable mutate() {
356        if (!mMutated && super.mutate() == this) {
357            mAnimationState.mutate();
358            mMutated = true;
359        }
360        return this;
361    }
362
363    @Override
364    AnimationState cloneConstantState() {
365        return new AnimationState(mAnimationState, this, null);
366    }
367
368    /**
369     * @hide
370     */
371    public void clearMutated() {
372        super.clearMutated();
373        mMutated = false;
374    }
375
376    private final static class AnimationState extends DrawableContainerState {
377        private int[] mDurations;
378        private boolean mOneShot = false;
379
380        AnimationState(AnimationState orig, AnimationDrawable owner, Resources res) {
381            super(orig, owner, res);
382
383            if (orig != null) {
384                mDurations = orig.mDurations;
385                mOneShot = orig.mOneShot;
386            } else {
387                mDurations = new int[getCapacity()];
388                mOneShot = false;
389            }
390        }
391
392        private void mutate() {
393            mDurations = mDurations.clone();
394        }
395
396        @Override
397        public Drawable newDrawable() {
398            return new AnimationDrawable(this, null);
399        }
400
401        @Override
402        public Drawable newDrawable(Resources res) {
403            return new AnimationDrawable(this, res);
404        }
405
406        public void addFrame(Drawable dr, int dur) {
407            // Do not combine the following. The array index must be evaluated before
408            // the array is accessed because super.addChild(dr) has a side effect on mDurations.
409            int pos = super.addChild(dr);
410            mDurations[pos] = dur;
411        }
412
413        @Override
414        public void growArray(int oldSize, int newSize) {
415            super.growArray(oldSize, newSize);
416            int[] newDurations = new int[newSize];
417            System.arraycopy(mDurations, 0, newDurations, 0, oldSize);
418            mDurations = newDurations;
419        }
420    }
421
422    @Override
423    protected void setConstantState(@NonNull DrawableContainerState state) {
424        super.setConstantState(state);
425
426        if (state instanceof AnimationState) {
427            mAnimationState = (AnimationState) state;
428        }
429    }
430
431    private AnimationDrawable(AnimationState state, Resources res) {
432        final AnimationState as = new AnimationState(state, this, res);
433        setConstantState(as);
434        if (state != null) {
435            setFrame(0, true, false);
436        }
437    }
438}
439
440