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