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 
17 package android.graphics.drawable;
18 
19 import com.android.internal.R;
20 
21 import java.io.IOException;
22 
23 import org.xmlpull.v1.XmlPullParser;
24 import org.xmlpull.v1.XmlPullParserException;
25 
26 import android.annotation.NonNull;
27 import android.content.res.Resources;
28 import android.content.res.TypedArray;
29 import android.content.res.Resources.Theme;
30 import android.os.SystemClock;
31 import 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  */
87 public 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 
AnimationDrawable()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
setVisible(boolean visible, boolean restart)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
start()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
stop()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
isRunning()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
run()196     public void run() {
197         nextFrame(false);
198     }
199 
200     @Override
unscheduleSelf(Runnable what)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      */
getNumberOfFrames()209     public int getNumberOfFrames() {
210         return mAnimationState.getChildCount();
211     }
212 
213     /**
214      * @return The Drawable at the specified frame index
215      */
getFrame(int index)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      */
getDuration(int i)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      */
isOneShot()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      */
setOneShot(boolean oneShot)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      */
addFrame(@onNull Drawable frame, int duration)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 
nextFrame(boolean unschedule)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 
setFrame(int frame, boolean unschedule, boolean animate)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
inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)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 
inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)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 
updateStateFromTypedArray(TypedArray a)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
mutate()360     public Drawable mutate() {
361         if (!mMutated && super.mutate() == this) {
362             mAnimationState.mutate();
363             mMutated = true;
364         }
365         return this;
366     }
367 
368     @Override
cloneConstantState()369     AnimationState cloneConstantState() {
370         return new AnimationState(mAnimationState, this, null);
371     }
372 
373     /**
374      * @hide
375      */
clearMutated()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 
AnimationState(AnimationState orig, AnimationDrawable owner, Resources res)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 
mutate()397         private void mutate() {
398             mDurations = mDurations.clone();
399         }
400 
401         @Override
newDrawable()402         public Drawable newDrawable() {
403             return new AnimationDrawable(this, null);
404         }
405 
406         @Override
newDrawable(Resources res)407         public Drawable newDrawable(Resources res) {
408             return new AnimationDrawable(this, res);
409         }
410 
addFrame(Drawable dr, int dur)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
growArray(int oldSize, int newSize)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
setConstantState(@onNull DrawableContainerState state)428     protected void setConstantState(@NonNull DrawableContainerState state) {
429         super.setConstantState(state);
430 
431         if (state instanceof AnimationState) {
432             mAnimationState = (AnimationState) state;
433         }
434     }
435 
AnimationDrawable(AnimationState state, Resources res)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