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); 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 super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedRotateDrawable_visible); 130 131 updateStateFromTypedArray(a); 132 inflateChildDrawable(r, parser, attrs, theme); 133 verifyRequiredAttributes(a); 134 a.recycle(); 135 136 updateLocalState(); 137 } 138 verifyRequiredAttributes(TypedArray a)139 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 140 // If we're not waiting on a theme, verify required attributes. 141 if (getDrawable() == null && (mState.mThemeAttrs == null 142 || mState.mThemeAttrs[R.styleable.AnimatedRotateDrawable_drawable] == 0)) { 143 throw new XmlPullParserException(a.getPositionDescription() 144 + ": <animated-rotate> tag requires a 'drawable' attribute or " 145 + "child tag defining a drawable"); 146 } 147 } 148 149 @Override updateStateFromTypedArray(TypedArray a)150 void updateStateFromTypedArray(TypedArray a) { 151 super.updateStateFromTypedArray(a); 152 153 final AnimatedRotateState state = mState; 154 155 if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotX)) { 156 final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotX); 157 state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION; 158 state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 159 } 160 161 if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotY)) { 162 final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotY); 163 state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION; 164 state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 165 } 166 167 setFramesCount(a.getInt( 168 R.styleable.AnimatedRotateDrawable_framesCount, state.mFramesCount)); 169 setFramesDuration(a.getInt( 170 R.styleable.AnimatedRotateDrawable_frameDuration, state.mFrameDuration)); 171 172 final Drawable dr = a.getDrawable(R.styleable.AnimatedRotateDrawable_drawable); 173 if (dr != null) { 174 setDrawable(dr); 175 } 176 } 177 178 @Override applyTheme(@ullable Theme t)179 public void applyTheme(@Nullable Theme t) { 180 final AnimatedRotateState state = mState; 181 if (state == null) { 182 return; 183 } 184 185 if (state.mThemeAttrs != null) { 186 final TypedArray a = t.resolveAttributes( 187 state.mThemeAttrs, R.styleable.AnimatedRotateDrawable); 188 try { 189 updateStateFromTypedArray(a); 190 verifyRequiredAttributes(a); 191 } catch (XmlPullParserException e) { 192 throw new RuntimeException(e); 193 } finally { 194 a.recycle(); 195 } 196 } 197 198 // The drawable may have changed as a result of applying the theme, so 199 // apply the theme to the wrapped drawable last. 200 super.applyTheme(t); 201 202 updateLocalState(); 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 static final class AnimatedRotateState extends DrawableWrapper.DrawableWrapperState { 215 boolean mPivotXRel = false; 216 float mPivotX = 0; 217 boolean mPivotYRel = false; 218 float mPivotY = 0; 219 int mFrameDuration = 150; 220 int mFramesCount = 12; 221 AnimatedRotateState(AnimatedRotateState orig)222 public AnimatedRotateState(AnimatedRotateState orig) { 223 super(orig); 224 225 if (orig != null) { 226 mPivotXRel = orig.mPivotXRel; 227 mPivotX = orig.mPivotX; 228 mPivotYRel = orig.mPivotYRel; 229 mPivotY = orig.mPivotY; 230 mFramesCount = orig.mFramesCount; 231 mFrameDuration = orig.mFrameDuration; 232 } 233 } 234 235 @Override newDrawable(Resources res)236 public Drawable newDrawable(Resources res) { 237 return new AnimatedRotateDrawable(this, res); 238 } 239 } 240 AnimatedRotateDrawable(AnimatedRotateState state, Resources res)241 private AnimatedRotateDrawable(AnimatedRotateState state, Resources res) { 242 super(state, res); 243 244 mState = state; 245 246 updateLocalState(); 247 } 248 updateLocalState()249 private void updateLocalState() { 250 final AnimatedRotateState state = mState; 251 mIncrement = 360.0f / state.mFramesCount; 252 253 // Force the wrapped drawable to use filtering and AA, if applicable, 254 // so that it looks smooth when rotated. 255 final Drawable drawable = getDrawable(); 256 if (drawable != null) { 257 drawable.setFilterBitmap(true); 258 if (drawable instanceof BitmapDrawable) { 259 ((BitmapDrawable) drawable).setAntiAlias(true); 260 } 261 } 262 } 263 264 private final Runnable mNextFrame = new Runnable() { 265 @Override 266 public void run() { 267 // TODO: This should be computed in draw(Canvas), based on the amount 268 // of time since the last frame drawn 269 mCurrentDegrees += mIncrement; 270 if (mCurrentDegrees > (360.0f - mIncrement)) { 271 mCurrentDegrees = 0.0f; 272 } 273 invalidateSelf(); 274 nextFrame(); 275 } 276 }; 277 } 278