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 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  */
84 public 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 
AnimationDrawable()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
setVisible(boolean visible, boolean restart)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
start()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
stop()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
isRunning()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
run()184     public void run() {
185         nextFrame(false);
186     }
187 
188     @Override
unscheduleSelf(Runnable what)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      */
getNumberOfFrames()198     public int getNumberOfFrames() {
199         return mAnimationState.getChildCount();
200     }
201 
202     /**
203      * @return The Drawable at the specified frame index
204      */
getFrame(int index)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      */
getDuration(int i)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      */
isOneShot()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      */
setOneShot(boolean oneShot)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      */
addFrame(Drawable frame, int duration)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 
nextFrame(boolean unschedule)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 
setFrame(int frame, boolean unschedule, boolean animate)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
inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)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 
inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)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 
updateStateFromTypedArray(TypedArray a)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
mutate()344     public Drawable mutate() {
345         if (!mMutated && super.mutate() == this) {
346             mAnimationState.mutate();
347             mMutated = true;
348         }
349         return this;
350     }
351 
352     @Override
cloneConstantState()353     AnimationState cloneConstantState() {
354         return new AnimationState(mAnimationState, this, null);
355     }
356 
357     /**
358      * @hide
359      */
clearMutated()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 
AnimationState(AnimationState orig, AnimationDrawable owner, Resources res)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 
mutate()382         private void mutate() {
383             mDurations = mDurations.clone();
384         }
385 
386         @Override
newDrawable()387         public Drawable newDrawable() {
388             return new AnimationDrawable(this, null);
389         }
390 
391         @Override
newDrawable(Resources res)392         public Drawable newDrawable(Resources res) {
393             return new AnimationDrawable(this, res);
394         }
395 
addFrame(Drawable dr, int dur)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
growArray(int oldSize, int newSize)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
setConstantState(@onNull DrawableContainerState state)413     protected void setConstantState(@NonNull DrawableContainerState state) {
414         super.setConstantState(state);
415 
416         if (state instanceof AnimationState) {
417             mAnimationState = (AnimationState) state;
418         }
419     }
420 
AnimationDrawable(AnimationState state, Resources res)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