| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.graphics.drawable; |
| |
| import android.annotation.NonNull; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.res.Resources; |
| import android.content.res.Resources.Theme; |
| import android.content.res.TypedArray; |
| import android.os.SystemClock; |
| import android.util.AttributeSet; |
| |
| import com.android.internal.R; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.IOException; |
| |
| /** |
| * An object used to create frame-by-frame animations, defined by a series of |
| * Drawable objects, which can be used as a View object's background. |
| * <p> |
| * The simplest way to create a frame-by-frame animation is to define the |
| * animation in an XML file, placed in the res/drawable/ folder, and set it as |
| * the background to a View object. Then, call {@link #start()} to run the |
| * animation. |
| * <p> |
| * An AnimationDrawable defined in XML consists of a single |
| * {@code <animation-list>} element and a series of nested |
| * {@code <item>} tags. Each item defines a frame of the animation. See |
| * the example below. |
| * <p> |
| * spin_animation.xml file in res/drawable/ folder: |
| * <pre> |
| * <!-- Animation frames are wheel0.png through wheel5.png |
| * files inside the res/drawable/ folder --> |
| * <animation-list android:id="@+id/selected" android:oneshot="false"> |
| * <item android:drawable="@drawable/wheel0" android:duration="50" /> |
| * <item android:drawable="@drawable/wheel1" android:duration="50" /> |
| * <item android:drawable="@drawable/wheel2" android:duration="50" /> |
| * <item android:drawable="@drawable/wheel3" android:duration="50" /> |
| * <item android:drawable="@drawable/wheel4" android:duration="50" /> |
| * <item android:drawable="@drawable/wheel5" android:duration="50" /> |
| * </animation-list></pre> |
| * <p> |
| * Here is the code to load and play this animation. |
| * <pre> |
| * // Load the ImageView that will host the animation and |
| * // set its background to our AnimationDrawable XML resource. |
| * ImageView img = (ImageView)findViewById(R.id.spinning_wheel_image); |
| * img.setBackgroundResource(R.drawable.spin_animation); |
| * |
| * // Get the background, which has been compiled to an AnimationDrawable object. |
| * AnimationDrawable frameAnimation = (AnimationDrawable) img.getBackground(); |
| * |
| * // Start the animation (looped playback by default). |
| * frameAnimation.start(); |
| * </pre> |
| * |
| * <div class="special reference"> |
| * <h3>Developer Guides</h3> |
| * <p>For more information about animating with {@code AnimationDrawable}, read the |
| * <a href="{@docRoot}guide/topics/graphics/drawable-animation.html">Drawable Animation</a> |
| * developer guide.</p> |
| * </div> |
| * |
| * @attr ref android.R.styleable#AnimationDrawable_visible |
| * @attr ref android.R.styleable#AnimationDrawable_variablePadding |
| * @attr ref android.R.styleable#AnimationDrawable_oneshot |
| * @attr ref android.R.styleable#AnimationDrawableItem_duration |
| * @attr ref android.R.styleable#AnimationDrawableItem_drawable |
| */ |
| public class AnimationDrawable extends DrawableContainer implements Runnable, Animatable { |
| private AnimationState mAnimationState; |
| |
| /** The current frame, ranging from 0 to {@link #mAnimationState#getChildCount() - 1} */ |
| @UnsupportedAppUsage |
| private int mCurFrame = 0; |
| |
| /** Whether the drawable has an animation callback posted. */ |
| private boolean mRunning; |
| |
| /** Whether the drawable should animate when visible. */ |
| private boolean mAnimating; |
| |
| private boolean mMutated; |
| |
| public AnimationDrawable() { |
| this(null, null); |
| } |
| |
| /** |
| * Sets whether this AnimationDrawable is visible. |
| * <p> |
| * When the drawable becomes invisible, it will pause its animation. A subsequent change to |
| * visible with <code>restart</code> set to true will restart the animation from the |
| * first frame. If <code>restart</code> is false, the drawable will resume from the most recent |
| * frame. If the drawable has already reached the last frame, it will then loop back to the |
| * first frame, unless it's a one shot drawable (set through {@link #setOneShot(boolean)}), |
| * in which case, it will stay on the last frame. |
| * |
| * @param visible true if visible, false otherwise |
| * @param restart when visible, true to force the animation to restart |
| * from the first frame |
| * @return true if the new visibility is different than its previous state |
| */ |
| @Override |
| public boolean setVisible(boolean visible, boolean restart) { |
| final boolean changed = super.setVisible(visible, restart); |
| if (visible) { |
| if (restart || changed) { |
| boolean startFromZero = restart || (!mRunning && !mAnimationState.mOneShot) || |
| mCurFrame >= mAnimationState.getChildCount(); |
| setFrame(startFromZero ? 0 : mCurFrame, true, mAnimating); |
| } |
| } else { |
| unscheduleSelf(this); |
| } |
| return changed; |
| } |
| |
| /** |
| * Starts the animation from the first frame, looping if necessary. This method has no effect |
| * if the animation is running. |
| * <p> |
| * <strong>Note:</strong> Do not call this in the |
| * {@link android.app.Activity#onCreate} method of your activity, because |
| * the {@link AnimationDrawable} is not yet fully attached to the window. |
| * If you want to play the animation immediately without requiring |
| * interaction, then you might want to call it from the |
| * {@link android.app.Activity#onWindowFocusChanged} method in your |
| * activity, which will get called when Android brings your window into |
| * focus. |
| * |
| * @see #isRunning() |
| * @see #stop() |
| */ |
| @Override |
| public void start() { |
| mAnimating = true; |
| |
| if (!isRunning()) { |
| // Start from 0th frame. |
| setFrame(0, false, mAnimationState.getChildCount() > 1 |
| || !mAnimationState.mOneShot); |
| } |
| } |
| |
| /** |
| * Stops the animation at the current frame. This method has no effect if the animation is not |
| * running. |
| * |
| * @see #isRunning() |
| * @see #start() |
| */ |
| @Override |
| public void stop() { |
| mAnimating = false; |
| |
| if (isRunning()) { |
| mCurFrame = 0; |
| unscheduleSelf(this); |
| } |
| } |
| |
| /** |
| * Indicates whether the animation is currently running or not. |
| * |
| * @return true if the animation is running, false otherwise |
| */ |
| @Override |
| public boolean isRunning() { |
| return mRunning; |
| } |
| |
| /** |
| * This method exists for implementation purpose only and should not be |
| * called directly. Invoke {@link #start()} instead. |
| * |
| * @see #start() |
| */ |
| @Override |
| public void run() { |
| nextFrame(false); |
| } |
| |
| @Override |
| public void unscheduleSelf(Runnable what) { |
| mRunning = false; |
| super.unscheduleSelf(what); |
| } |
| |
| /** |
| * @return The number of frames in the animation |
| */ |
| public int getNumberOfFrames() { |
| return mAnimationState.getChildCount(); |
| } |
| |
| /** |
| * @return The Drawable at the specified frame index |
| */ |
| public Drawable getFrame(int index) { |
| return mAnimationState.getChild(index); |
| } |
| |
| /** |
| * @return The duration in milliseconds of the frame at the |
| * specified index |
| */ |
| public int getDuration(int i) { |
| return mAnimationState.mDurations[i]; |
| } |
| |
| /** |
| * @return True of the animation will play once, false otherwise |
| */ |
| public boolean isOneShot() { |
| return mAnimationState.mOneShot; |
| } |
| |
| /** |
| * Sets whether the animation should play once or repeat. |
| * |
| * @param oneShot Pass true if the animation should only play once |
| */ |
| public void setOneShot(boolean oneShot) { |
| mAnimationState.mOneShot = oneShot; |
| } |
| |
| /** |
| * Adds a frame to the animation |
| * |
| * @param frame The frame to add |
| * @param duration How long in milliseconds the frame should appear |
| */ |
| public void addFrame(@NonNull Drawable frame, int duration) { |
| mAnimationState.addFrame(frame, duration); |
| if (!mRunning) { |
| setFrame(0, true, false); |
| } |
| } |
| |
| private void nextFrame(boolean unschedule) { |
| int nextFrame = mCurFrame + 1; |
| final int numFrames = mAnimationState.getChildCount(); |
| final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1); |
| |
| // Loop if necessary. One-shot animations should never hit this case. |
| if (!mAnimationState.mOneShot && nextFrame >= numFrames) { |
| nextFrame = 0; |
| } |
| |
| setFrame(nextFrame, unschedule, !isLastFrame); |
| } |
| |
| private void setFrame(int frame, boolean unschedule, boolean animate) { |
| if (frame >= mAnimationState.getChildCount()) { |
| return; |
| } |
| mAnimating = animate; |
| mCurFrame = frame; |
| selectDrawable(frame); |
| if (unschedule || animate) { |
| unscheduleSelf(this); |
| } |
| if (animate) { |
| // Unscheduling may have clobbered these values; restore them |
| mCurFrame = frame; |
| mRunning = true; |
| scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]); |
| } |
| } |
| |
| @Override |
| public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) |
| throws XmlPullParserException, IOException { |
| final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable); |
| super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible); |
| updateStateFromTypedArray(a); |
| updateDensity(r); |
| a.recycle(); |
| |
| inflateChildElements(r, parser, attrs, theme); |
| |
| setFrame(0, true, false); |
| } |
| |
| private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, |
| Theme theme) throws XmlPullParserException, IOException { |
| int type; |
| |
| final int innerDepth = parser.getDepth()+1; |
| int depth; |
| while ((type=parser.next()) != XmlPullParser.END_DOCUMENT |
| && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { |
| if (type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| if (depth > innerDepth || !parser.getName().equals("item")) { |
| continue; |
| } |
| |
| final TypedArray a = obtainAttributes(r, theme, attrs, |
| R.styleable.AnimationDrawableItem); |
| |
| final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1); |
| if (duration < 0) { |
| throw new XmlPullParserException(parser.getPositionDescription() |
| + ": <item> tag requires a 'duration' attribute"); |
| } |
| |
| Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable); |
| |
| a.recycle(); |
| |
| if (dr == null) { |
| while ((type=parser.next()) == XmlPullParser.TEXT) { |
| // Empty |
| } |
| if (type != XmlPullParser.START_TAG) { |
| throw new XmlPullParserException(parser.getPositionDescription() |
| + ": <item> tag requires a 'drawable' attribute or child tag" |
| + " defining a drawable"); |
| } |
| dr = Drawable.createFromXmlInner(r, parser, attrs, theme); |
| } |
| |
| mAnimationState.addFrame(dr, duration); |
| if (dr != null) { |
| dr.setCallback(this); |
| } |
| } |
| } |
| |
| private void updateStateFromTypedArray(TypedArray a) { |
| mAnimationState.mVariablePadding = a.getBoolean( |
| R.styleable.AnimationDrawable_variablePadding, mAnimationState.mVariablePadding); |
| |
| mAnimationState.mOneShot = a.getBoolean( |
| R.styleable.AnimationDrawable_oneshot, mAnimationState.mOneShot); |
| } |
| |
| @Override |
| @NonNull |
| public Drawable mutate() { |
| if (!mMutated && super.mutate() == this) { |
| mAnimationState.mutate(); |
| mMutated = true; |
| } |
| return this; |
| } |
| |
| @Override |
| AnimationState cloneConstantState() { |
| return new AnimationState(mAnimationState, this, null); |
| } |
| |
| /** |
| * @hide |
| */ |
| public void clearMutated() { |
| super.clearMutated(); |
| mMutated = false; |
| } |
| |
| private final static class AnimationState extends DrawableContainerState { |
| private int[] mDurations; |
| private boolean mOneShot = false; |
| |
| AnimationState(AnimationState orig, AnimationDrawable owner, Resources res) { |
| super(orig, owner, res); |
| |
| if (orig != null) { |
| mDurations = orig.mDurations; |
| mOneShot = orig.mOneShot; |
| } else { |
| mDurations = new int[getCapacity()]; |
| mOneShot = false; |
| } |
| } |
| |
| private void mutate() { |
| mDurations = mDurations.clone(); |
| } |
| |
| @Override |
| public Drawable newDrawable() { |
| return new AnimationDrawable(this, null); |
| } |
| |
| @Override |
| public Drawable newDrawable(Resources res) { |
| return new AnimationDrawable(this, res); |
| } |
| |
| public void addFrame(Drawable dr, int dur) { |
| // Do not combine the following. The array index must be evaluated before |
| // the array is accessed because super.addChild(dr) has a side effect on mDurations. |
| int pos = super.addChild(dr); |
| mDurations[pos] = dur; |
| } |
| |
| @Override |
| public void growArray(int oldSize, int newSize) { |
| super.growArray(oldSize, newSize); |
| int[] newDurations = new int[newSize]; |
| System.arraycopy(mDurations, 0, newDurations, 0, oldSize); |
| mDurations = newDurations; |
| } |
| |
| public long getTotalDuration() { |
| if (mDurations != null) { |
| int total = 0; |
| for (int dur : mDurations) { |
| total += dur; |
| } |
| return total; |
| } |
| return 0; |
| } |
| } |
| |
| @Override |
| protected void setConstantState(@NonNull DrawableContainerState state) { |
| super.setConstantState(state); |
| |
| if (state instanceof AnimationState) { |
| mAnimationState = (AnimationState) state; |
| } |
| } |
| |
| /** |
| * Gets the total duration of the animation |
| * @hide |
| */ |
| public long getTotalDuration() { |
| return mAnimationState.getTotalDuration(); |
| } |
| |
| private AnimationDrawable(AnimationState state, Resources res) { |
| final AnimationState as = new AnimationState(state, this, res); |
| setConstantState(as); |
| if (state != null) { |
| setFrame(0, true, false); |
| } |
| } |
| } |
| |