1 /* 2 * Copyright (C) 2009 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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.res.Resources; 23 import android.content.res.Resources.Theme; 24 import android.content.res.TypedArray; 25 import android.graphics.Canvas; 26 import android.graphics.Rect; 27 import android.os.SystemClock; 28 import android.util.AttributeSet; 29 import android.util.TypedValue; 30 31 import com.android.internal.R; 32 33 import org.xmlpull.v1.XmlPullParser; 34 import org.xmlpull.v1.XmlPullParserException; 35 36 import java.io.IOException; 37 38 /** 39 * @hide 40 */ 41 public class AnimatedRotateDrawable extends DrawableWrapper implements Animatable { 42 private AnimatedRotateState mState; 43 44 private float mCurrentDegrees; 45 private float mIncrement; 46 47 /** Whether this drawable is currently animating. */ 48 private boolean mRunning; 49 50 /** 51 * Creates a new animated rotating drawable with no wrapped drawable. 52 */ AnimatedRotateDrawable()53 public AnimatedRotateDrawable() { 54 this(new AnimatedRotateState(null, null), null); 55 } 56 57 @Override draw(Canvas canvas)58 public void draw(Canvas canvas) { 59 final Drawable drawable = getDrawable(); 60 final Rect bounds = drawable.getBounds(); 61 final int w = bounds.right - bounds.left; 62 final int h = bounds.bottom - bounds.top; 63 64 final AnimatedRotateState st = mState; 65 final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX; 66 final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY; 67 68 final int saveCount = canvas.save(); 69 canvas.rotate(mCurrentDegrees, px + bounds.left, py + bounds.top); 70 drawable.draw(canvas); 71 canvas.restoreToCount(saveCount); 72 } 73 74 /** 75 * Starts the rotation animation. 76 * <p> 77 * The animation will run until {@link #stop()} is called. Calling this 78 * method while the animation is already running has no effect. 79 * 80 * @see #stop() 81 */ 82 @Override start()83 public void start() { 84 if (!mRunning) { 85 mRunning = true; 86 nextFrame(); 87 } 88 } 89 90 /** 91 * Stops the rotation animation. 92 * 93 * @see #start() 94 */ 95 @Override stop()96 public void stop() { 97 mRunning = false; 98 unscheduleSelf(mNextFrame); 99 } 100 101 @Override isRunning()102 public boolean isRunning() { 103 return mRunning; 104 } 105 nextFrame()106 private void nextFrame() { 107 unscheduleSelf(mNextFrame); 108 scheduleSelf(mNextFrame, SystemClock.uptimeMillis() + mState.mFrameDuration); 109 } 110 111 @Override setVisible(boolean visible, boolean restart)112 public boolean setVisible(boolean visible, boolean restart) { 113 final boolean changed = super.setVisible(visible, restart); 114 if (visible) { 115 if (changed || restart) { 116 mCurrentDegrees = 0.0f; 117 nextFrame(); 118 } 119 } else { 120 unscheduleSelf(mNextFrame); 121 } 122 return changed; 123 } 124 125 @Override inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)126 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 127 @NonNull AttributeSet attrs, @Nullable Theme theme) 128 throws XmlPullParserException, IOException { 129 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedRotateDrawable); 130 131 // Inflation will advance the XmlPullParser and AttributeSet. 132 super.inflate(r, parser, attrs, theme); 133 134 updateStateFromTypedArray(a); 135 verifyRequiredAttributes(a); 136 a.recycle(); 137 138 updateLocalState(); 139 } 140 141 @Override applyTheme(@onNull Theme t)142 public void applyTheme(@NonNull Theme t) { 143 super.applyTheme(t); 144 145 final AnimatedRotateState state = mState; 146 if (state == null) { 147 return; 148 } 149 150 if (state.mThemeAttrs != null) { 151 final TypedArray a = t.resolveAttributes( 152 state.mThemeAttrs, R.styleable.AnimatedRotateDrawable); 153 try { 154 updateStateFromTypedArray(a); 155 verifyRequiredAttributes(a); 156 } catch (XmlPullParserException e) { 157 rethrowAsRuntimeException(e); 158 } finally { 159 a.recycle(); 160 } 161 } 162 163 updateLocalState(); 164 } 165 verifyRequiredAttributes(@onNull TypedArray a)166 private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException { 167 // If we're not waiting on a theme, verify required attributes. 168 if (getDrawable() == null && (mState.mThemeAttrs == null 169 || mState.mThemeAttrs[R.styleable.AnimatedRotateDrawable_drawable] == 0)) { 170 throw new XmlPullParserException(a.getPositionDescription() 171 + ": <animated-rotate> tag requires a 'drawable' attribute or " 172 + "child tag defining a drawable"); 173 } 174 } 175 updateStateFromTypedArray(@onNull TypedArray a)176 private void updateStateFromTypedArray(@NonNull TypedArray a) { 177 final AnimatedRotateState state = mState; 178 if (state == null) { 179 return; 180 } 181 182 // Account for any configuration changes. 183 state.mChangingConfigurations |= a.getChangingConfigurations(); 184 185 // Extract the theme attributes, if any. 186 state.mThemeAttrs = a.extractThemeAttrs(); 187 188 if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotX)) { 189 final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotX); 190 state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION; 191 state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 192 } 193 194 if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotY)) { 195 final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotY); 196 state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION; 197 state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 198 } 199 200 setFramesCount(a.getInt( 201 R.styleable.AnimatedRotateDrawable_framesCount, state.mFramesCount)); 202 setFramesDuration(a.getInt( 203 R.styleable.AnimatedRotateDrawable_frameDuration, state.mFrameDuration)); 204 } 205 206 @UnsupportedAppUsage setFramesCount(int framesCount)207 public void setFramesCount(int framesCount) { 208 mState.mFramesCount = framesCount; 209 mIncrement = 360.0f / mState.mFramesCount; 210 } 211 212 @UnsupportedAppUsage setFramesDuration(int framesDuration)213 public void setFramesDuration(int framesDuration) { 214 mState.mFrameDuration = framesDuration; 215 } 216 217 @Override mutateConstantState()218 DrawableWrapperState mutateConstantState() { 219 mState = new AnimatedRotateState(mState, null); 220 return mState; 221 } 222 223 static final class AnimatedRotateState extends DrawableWrapper.DrawableWrapperState { 224 private int[] mThemeAttrs; 225 226 boolean mPivotXRel = false; 227 float mPivotX = 0; 228 boolean mPivotYRel = false; 229 float mPivotY = 0; 230 int mFrameDuration = 150; 231 int mFramesCount = 12; 232 AnimatedRotateState(AnimatedRotateState orig, Resources res)233 public AnimatedRotateState(AnimatedRotateState orig, Resources res) { 234 super(orig, res); 235 236 if (orig != null) { 237 mPivotXRel = orig.mPivotXRel; 238 mPivotX = orig.mPivotX; 239 mPivotYRel = orig.mPivotYRel; 240 mPivotY = orig.mPivotY; 241 mFramesCount = orig.mFramesCount; 242 mFrameDuration = orig.mFrameDuration; 243 } 244 } 245 246 @Override newDrawable(Resources res)247 public Drawable newDrawable(Resources res) { 248 return new AnimatedRotateDrawable(this, res); 249 } 250 } 251 AnimatedRotateDrawable(AnimatedRotateState state, Resources res)252 private AnimatedRotateDrawable(AnimatedRotateState state, Resources res) { 253 super(state, res); 254 255 mState = state; 256 257 updateLocalState(); 258 } 259 updateLocalState()260 private void updateLocalState() { 261 final AnimatedRotateState state = mState; 262 mIncrement = 360.0f / state.mFramesCount; 263 264 // Force the wrapped drawable to use filtering and AA, if applicable, 265 // so that it looks smooth when rotated. 266 final Drawable drawable = getDrawable(); 267 if (drawable != null) { 268 drawable.setFilterBitmap(true); 269 if (drawable instanceof BitmapDrawable) { 270 ((BitmapDrawable) drawable).setAntiAlias(true); 271 } 272 } 273 } 274 275 private final Runnable mNextFrame = new Runnable() { 276 @Override 277 public void run() { 278 // TODO: This should be computed in draw(Canvas), based on the amount 279 // of time since the last frame drawn 280 mCurrentDegrees += mIncrement; 281 if (mCurrentDegrees > (360.0f - mIncrement)) { 282 mCurrentDegrees = 0.0f; 283 } 284 invalidateSelf(); 285 nextFrame(); 286 } 287 }; 288 } 289