• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 &lt;animation-list&gt;} element and a series of nested
44  * {@code &lt;item&gt;} 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
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
setVisible(boolean visible, boolean restart)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
start()150     public void start() {
151         mAnimating = true;
152 
153         if (!isRunning()) {
154             // Start from 0th frame.
155             setFrame(0, false, mAnimationState.getChildCount() > 1
156                     || !mAnimationState.mOneShot);
157         }
158     }
159 
160     /**
161      * Stops the animation. This method has no effect if the animation is not
162      * running.
163      *
164      * @see #isRunning()
165      * @see #start()
166      */
167     @Override
stop()168     public void stop() {
169         mAnimating = false;
170 
171         if (isRunning()) {
172             unscheduleSelf(this);
173         }
174     }
175 
176     /**
177      * Indicates whether the animation is currently running or not.
178      *
179      * @return true if the animation is running, false otherwise
180      */
181     @Override
isRunning()182     public boolean isRunning() {
183         return mRunning;
184     }
185 
186     /**
187      * This method exists for implementation purpose only and should not be
188      * called directly. Invoke {@link #start()} instead.
189      *
190      * @see #start()
191      */
192     @Override
run()193     public void run() {
194         nextFrame(false);
195     }
196 
197     @Override
unscheduleSelf(Runnable what)198     public void unscheduleSelf(Runnable what) {
199         mCurFrame = 0;
200         mRunning = false;
201         super.unscheduleSelf(what);
202     }
203 
204     /**
205      * @return The number of frames in the animation
206      */
getNumberOfFrames()207     public int getNumberOfFrames() {
208         return mAnimationState.getChildCount();
209     }
210 
211     /**
212      * @return The Drawable at the specified frame index
213      */
getFrame(int index)214     public Drawable getFrame(int index) {
215         return mAnimationState.getChild(index);
216     }
217 
218     /**
219      * @return The duration in milliseconds of the frame at the
220      *         specified index
221      */
getDuration(int i)222     public int getDuration(int i) {
223         return mAnimationState.mDurations[i];
224     }
225 
226     /**
227      * @return True of the animation will play once, false otherwise
228      */
isOneShot()229     public boolean isOneShot() {
230         return mAnimationState.mOneShot;
231     }
232 
233     /**
234      * Sets whether the animation should play once or repeat.
235      *
236      * @param oneShot Pass true if the animation should only play once
237      */
setOneShot(boolean oneShot)238     public void setOneShot(boolean oneShot) {
239         mAnimationState.mOneShot = oneShot;
240     }
241 
242     /**
243      * Adds a frame to the animation
244      *
245      * @param frame The frame to add
246      * @param duration How long in milliseconds the frame should appear
247      */
addFrame(@onNull Drawable frame, int duration)248     public void addFrame(@NonNull Drawable frame, int duration) {
249         mAnimationState.addFrame(frame, duration);
250         if (!mRunning) {
251             setFrame(0, true, false);
252         }
253     }
254 
nextFrame(boolean unschedule)255     private void nextFrame(boolean unschedule) {
256         int nextFrame = mCurFrame + 1;
257         final int numFrames = mAnimationState.getChildCount();
258         final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1);
259 
260         // Loop if necessary. One-shot animations should never hit this case.
261         if (!mAnimationState.mOneShot && nextFrame >= numFrames) {
262             nextFrame = 0;
263         }
264 
265         setFrame(nextFrame, unschedule, !isLastFrame);
266     }
267 
setFrame(int frame, boolean unschedule, boolean animate)268     private void setFrame(int frame, boolean unschedule, boolean animate) {
269         if (frame >= mAnimationState.getChildCount()) {
270             return;
271         }
272         mAnimating = animate;
273         mCurFrame = frame;
274         selectDrawable(frame);
275         if (unschedule || animate) {
276             unscheduleSelf(this);
277         }
278         if (animate) {
279             // Unscheduling may have clobbered these values; restore them
280             mCurFrame = frame;
281             mRunning = true;
282             scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
283         }
284     }
285 
286     @Override
inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)287     public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
288             throws XmlPullParserException, IOException {
289         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable);
290         super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible);
291         updateStateFromTypedArray(a);
292         a.recycle();
293 
294         inflateChildElements(r, parser, attrs, theme);
295 
296         setFrame(0, true, false);
297     }
298 
inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)299     private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
300             Theme theme) throws XmlPullParserException, IOException {
301         int type;
302 
303         final int innerDepth = parser.getDepth()+1;
304         int depth;
305         while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
306                 && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
307             if (type != XmlPullParser.START_TAG) {
308                 continue;
309             }
310 
311             if (depth > innerDepth || !parser.getName().equals("item")) {
312                 continue;
313             }
314 
315             final TypedArray a = obtainAttributes(r, theme, attrs,
316                     R.styleable.AnimationDrawableItem);
317 
318             final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1);
319             if (duration < 0) {
320                 throw new XmlPullParserException(parser.getPositionDescription()
321                         + ": <item> tag requires a 'duration' attribute");
322             }
323 
324             Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable);
325 
326             a.recycle();
327 
328             if (dr == null) {
329                 while ((type=parser.next()) == XmlPullParser.TEXT) {
330                     // Empty
331                 }
332                 if (type != XmlPullParser.START_TAG) {
333                     throw new XmlPullParserException(parser.getPositionDescription()
334                             + ": <item> tag requires a 'drawable' attribute or child tag"
335                             + " defining a drawable");
336                 }
337                 dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
338             }
339 
340             mAnimationState.addFrame(dr, duration);
341             if (dr != null) {
342                 dr.setCallback(this);
343             }
344         }
345     }
346 
updateStateFromTypedArray(TypedArray a)347     private void updateStateFromTypedArray(TypedArray a) {
348         mAnimationState.mVariablePadding = a.getBoolean(
349                 R.styleable.AnimationDrawable_variablePadding, mAnimationState.mVariablePadding);
350 
351         mAnimationState.mOneShot = a.getBoolean(
352                 R.styleable.AnimationDrawable_oneshot, mAnimationState.mOneShot);
353     }
354 
355     @Override
356     @NonNull
mutate()357     public Drawable mutate() {
358         if (!mMutated && super.mutate() == this) {
359             mAnimationState.mutate();
360             mMutated = true;
361         }
362         return this;
363     }
364 
365     @Override
cloneConstantState()366     AnimationState cloneConstantState() {
367         return new AnimationState(mAnimationState, this, null);
368     }
369 
370     /**
371      * @hide
372      */
clearMutated()373     public void clearMutated() {
374         super.clearMutated();
375         mMutated = false;
376     }
377 
378     private final static class AnimationState extends DrawableContainerState {
379         private int[] mDurations;
380         private boolean mOneShot = false;
381 
AnimationState(AnimationState orig, AnimationDrawable owner, Resources res)382         AnimationState(AnimationState orig, AnimationDrawable owner, Resources res) {
383             super(orig, owner, res);
384 
385             if (orig != null) {
386                 mDurations = orig.mDurations;
387                 mOneShot = orig.mOneShot;
388             } else {
389                 mDurations = new int[getCapacity()];
390                 mOneShot = false;
391             }
392         }
393 
mutate()394         private void mutate() {
395             mDurations = mDurations.clone();
396         }
397 
398         @Override
newDrawable()399         public Drawable newDrawable() {
400             return new AnimationDrawable(this, null);
401         }
402 
403         @Override
newDrawable(Resources res)404         public Drawable newDrawable(Resources res) {
405             return new AnimationDrawable(this, res);
406         }
407 
addFrame(Drawable dr, int dur)408         public void addFrame(Drawable dr, int dur) {
409             // Do not combine the following. The array index must be evaluated before
410             // the array is accessed because super.addChild(dr) has a side effect on mDurations.
411             int pos = super.addChild(dr);
412             mDurations[pos] = dur;
413         }
414 
415         @Override
growArray(int oldSize, int newSize)416         public void growArray(int oldSize, int newSize) {
417             super.growArray(oldSize, newSize);
418             int[] newDurations = new int[newSize];
419             System.arraycopy(mDurations, 0, newDurations, 0, oldSize);
420             mDurations = newDurations;
421         }
422     }
423 
424     @Override
setConstantState(@onNull DrawableContainerState state)425     protected void setConstantState(@NonNull DrawableContainerState state) {
426         super.setConstantState(state);
427 
428         if (state instanceof AnimationState) {
429             mAnimationState = (AnimationState) state;
430         }
431     }
432 
AnimationDrawable(AnimationState state, Resources res)433     private AnimationDrawable(AnimationState state, Resources res) {
434         final AnimationState as = new AnimationState(state, this, res);
435         setConstantState(as);
436         if (state != null) {
437             setFrame(0, true, false);
438         }
439     }
440 }
441 
442