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