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