Home | History | Annotate | Download | only in drawable
      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 
    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
    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
    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
    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
    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
    196     public void run() {
    197         nextFrame(false);
    198     }
    199 
    200     @Override
    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      */
    209     public int getNumberOfFrames() {
    210         return mAnimationState.getChildCount();
    211     }
    212 
    213     /**
    214      * @return The Drawable at the specified frame index
    215      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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
    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 
    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 
    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
    360     public Drawable mutate() {
    361         if (!mMutated && super.mutate() == this) {
    362             mAnimationState.mutate();
    363             mMutated = true;
    364         }
    365         return this;
    366     }
    367 
    368     @Override
    369     AnimationState cloneConstantState() {
    370         return new AnimationState(mAnimationState, this, null);
    371     }
    372 
    373     /**
    374      * @hide
    375      */
    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 
    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 
    397         private void mutate() {
    398             mDurations = mDurations.clone();
    399         }
    400 
    401         @Override
    402         public Drawable newDrawable() {
    403             return new AnimationDrawable(this, null);
    404         }
    405 
    406         @Override
    407         public Drawable newDrawable(Resources res) {
    408             return new AnimationDrawable(this, res);
    409         }
    410 
    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
    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
    428     protected void setConstantState(@NonNull DrawableContainerState state) {
    429         super.setConstantState(state);
    430 
    431         if (state instanceof AnimationState) {
    432             mAnimationState = (AnimationState) state;
    433         }
    434     }
    435 
    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