/* * Copyright (C) 2009 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.annotation.Nullable; import android.graphics.Canvas; import android.graphics.Rect; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.Resources.Theme; import android.util.AttributeSet; import android.util.TypedValue; import android.os.SystemClock; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import com.android.internal.R; /** * @hide */ public class AnimatedRotateDrawable extends DrawableWrapper implements Animatable { private AnimatedRotateState mState; private float mCurrentDegrees; private float mIncrement; /** Whether this drawable is currently animating. */ private boolean mRunning; /** * Creates a new animated rotating drawable with no wrapped drawable. */ public AnimatedRotateDrawable() { this(new AnimatedRotateState(null, null), null); } @Override public void draw(Canvas canvas) { final Drawable drawable = getDrawable(); final Rect bounds = drawable.getBounds(); final int w = bounds.right - bounds.left; final int h = bounds.bottom - bounds.top; final AnimatedRotateState st = mState; final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX; final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY; final int saveCount = canvas.save(); canvas.rotate(mCurrentDegrees, px + bounds.left, py + bounds.top); drawable.draw(canvas); canvas.restoreToCount(saveCount); } /** * Starts the rotation animation. *

* The animation will run until {@link #stop()} is called. Calling this * method while the animation is already running has no effect. * * @see #stop() */ @Override public void start() { if (!mRunning) { mRunning = true; nextFrame(); } } /** * Stops the rotation animation. * * @see #start() */ @Override public void stop() { mRunning = false; unscheduleSelf(mNextFrame); } @Override public boolean isRunning() { return mRunning; } private void nextFrame() { unscheduleSelf(mNextFrame); scheduleSelf(mNextFrame, SystemClock.uptimeMillis() + mState.mFrameDuration); } @Override public boolean setVisible(boolean visible, boolean restart) { final boolean changed = super.setVisible(visible, restart); if (visible) { if (changed || restart) { mCurrentDegrees = 0.0f; nextFrame(); } } else { unscheduleSelf(mNextFrame); } return changed; } @Override public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedRotateDrawable); // Inflation will advance the XmlPullParser and AttributeSet. super.inflate(r, parser, attrs, theme); updateStateFromTypedArray(a); verifyRequiredAttributes(a); a.recycle(); updateLocalState(); } @Override public void applyTheme(@NonNull Theme t) { super.applyTheme(t); final AnimatedRotateState state = mState; if (state == null) { return; } if (state.mThemeAttrs != null) { final TypedArray a = t.resolveAttributes( state.mThemeAttrs, R.styleable.AnimatedRotateDrawable); try { updateStateFromTypedArray(a); verifyRequiredAttributes(a); } catch (XmlPullParserException e) { rethrowAsRuntimeException(e); } finally { a.recycle(); } } updateLocalState(); } private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException { // If we're not waiting on a theme, verify required attributes. if (getDrawable() == null && (mState.mThemeAttrs == null || mState.mThemeAttrs[R.styleable.AnimatedRotateDrawable_drawable] == 0)) { throw new XmlPullParserException(a.getPositionDescription() + ": tag requires a 'drawable' attribute or " + "child tag defining a drawable"); } } private void updateStateFromTypedArray(@NonNull TypedArray a) { final AnimatedRotateState state = mState; if (state == null) { return; } // Account for any configuration changes. state.mChangingConfigurations |= a.getChangingConfigurations(); // Extract the theme attributes, if any. state.mThemeAttrs = a.extractThemeAttrs(); if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotX)) { final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotX); state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION; state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); } if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotY)) { final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotY); state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION; state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); } setFramesCount(a.getInt( R.styleable.AnimatedRotateDrawable_framesCount, state.mFramesCount)); setFramesDuration(a.getInt( R.styleable.AnimatedRotateDrawable_frameDuration, state.mFrameDuration)); } public void setFramesCount(int framesCount) { mState.mFramesCount = framesCount; mIncrement = 360.0f / mState.mFramesCount; } public void setFramesDuration(int framesDuration) { mState.mFrameDuration = framesDuration; } @Override DrawableWrapperState mutateConstantState() { mState = new AnimatedRotateState(mState, null); return mState; } static final class AnimatedRotateState extends DrawableWrapper.DrawableWrapperState { private int[] mThemeAttrs; boolean mPivotXRel = false; float mPivotX = 0; boolean mPivotYRel = false; float mPivotY = 0; int mFrameDuration = 150; int mFramesCount = 12; public AnimatedRotateState(AnimatedRotateState orig, Resources res) { super(orig, res); if (orig != null) { mPivotXRel = orig.mPivotXRel; mPivotX = orig.mPivotX; mPivotYRel = orig.mPivotYRel; mPivotY = orig.mPivotY; mFramesCount = orig.mFramesCount; mFrameDuration = orig.mFrameDuration; } } @Override public Drawable newDrawable(Resources res) { return new AnimatedRotateDrawable(this, res); } } private AnimatedRotateDrawable(AnimatedRotateState state, Resources res) { super(state, res); mState = state; updateLocalState(); } private void updateLocalState() { final AnimatedRotateState state = mState; mIncrement = 360.0f / state.mFramesCount; // Force the wrapped drawable to use filtering and AA, if applicable, // so that it looks smooth when rotated. final Drawable drawable = getDrawable(); if (drawable != null) { drawable.setFilterBitmap(true); if (drawable instanceof BitmapDrawable) { ((BitmapDrawable) drawable).setAntiAlias(true); } } } private final Runnable mNextFrame = new Runnable() { @Override public void run() { // TODO: This should be computed in draw(Canvas), based on the amount // of time since the last frame drawn mCurrentDegrees += mIncrement; if (mCurrentDegrees > (360.0f - mIncrement)) { mCurrentDegrees = 0.0f; } invalidateSelf(); nextFrame(); } }; }