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.graphics.ColorFilter; 24 import android.graphics.PorterDuff.Mode; 25 import android.content.res.ColorStateList; 26 import android.content.res.Resources; 27 import android.content.res.TypedArray; 28 import android.content.res.Resources.Theme; 29 import android.util.AttributeSet; 30 import android.util.TypedValue; 31 import android.util.Log; 32 import android.os.SystemClock; 33 34 import org.xmlpull.v1.XmlPullParser; 35 import org.xmlpull.v1.XmlPullParserException; 36 37 import java.io.IOException; 38 39 import com.android.internal.R; 40 41 /** 42 * @hide 43 */ 44 public class AnimatedRotateDrawable extends Drawable implements Drawable.Callback, Runnable, 45 Animatable { 46 private static final String TAG = "AnimatedRotateDrawable"; 47 48 private AnimatedRotateState mState; 49 private boolean mMutated; 50 private float mCurrentDegrees; 51 private float mIncrement; 52 private boolean mRunning; 53 54 public AnimatedRotateDrawable() { 55 this(null, null); 56 } 57 58 private AnimatedRotateDrawable(AnimatedRotateState rotateState, Resources res) { 59 mState = new AnimatedRotateState(rotateState, this, res); 60 init(); 61 } 62 63 private void init() { 64 final AnimatedRotateState state = mState; 65 mIncrement = 360.0f / state.mFramesCount; 66 final Drawable drawable = state.mDrawable; 67 if (drawable != null) { 68 drawable.setFilterBitmap(true); 69 if (drawable instanceof BitmapDrawable) { 70 ((BitmapDrawable) drawable).setAntiAlias(true); 71 } 72 } 73 } 74 75 @Override 76 public void draw(Canvas canvas) { 77 int saveCount = canvas.save(); 78 79 final AnimatedRotateState st = mState; 80 final Drawable drawable = st.mDrawable; 81 final Rect bounds = drawable.getBounds(); 82 83 int w = bounds.right - bounds.left; 84 int h = bounds.bottom - bounds.top; 85 86 float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX; 87 float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY; 88 89 canvas.rotate(mCurrentDegrees, px + bounds.left, py + bounds.top); 90 91 drawable.draw(canvas); 92 93 canvas.restoreToCount(saveCount); 94 } 95 96 @Override 97 public void start() { 98 if (!mRunning) { 99 mRunning = true; 100 nextFrame(); 101 } 102 } 103 104 @Override 105 public void stop() { 106 mRunning = false; 107 unscheduleSelf(this); 108 } 109 110 @Override 111 public boolean isRunning() { 112 return mRunning; 113 } 114 115 private void nextFrame() { 116 unscheduleSelf(this); 117 scheduleSelf(this, SystemClock.uptimeMillis() + mState.mFrameDuration); 118 } 119 120 @Override 121 public void run() { 122 // TODO: This should be computed in draw(Canvas), based on the amount 123 // of time since the last frame drawn 124 mCurrentDegrees += mIncrement; 125 if (mCurrentDegrees > (360.0f - mIncrement)) { 126 mCurrentDegrees = 0.0f; 127 } 128 invalidateSelf(); 129 nextFrame(); 130 } 131 132 @Override 133 public boolean setVisible(boolean visible, boolean restart) { 134 mState.mDrawable.setVisible(visible, restart); 135 boolean changed = super.setVisible(visible, restart); 136 if (visible) { 137 if (changed || restart) { 138 mCurrentDegrees = 0.0f; 139 nextFrame(); 140 } 141 } else { 142 unscheduleSelf(this); 143 } 144 return changed; 145 } 146 147 /** 148 * Returns the drawable rotated by this RotateDrawable. 149 */ 150 public Drawable getDrawable() { 151 return mState.mDrawable; 152 } 153 154 @Override 155 public int getChangingConfigurations() { 156 return super.getChangingConfigurations() 157 | mState.mChangingConfigurations 158 | mState.mDrawable.getChangingConfigurations(); 159 } 160 161 @Override 162 public void setAlpha(int alpha) { 163 mState.mDrawable.setAlpha(alpha); 164 } 165 166 @Override 167 public int getAlpha() { 168 return mState.mDrawable.getAlpha(); 169 } 170 171 @Override 172 public void setColorFilter(ColorFilter cf) { 173 mState.mDrawable.setColorFilter(cf); 174 } 175 176 @Override 177 public void setTintList(ColorStateList tint) { 178 mState.mDrawable.setTintList(tint); 179 } 180 181 @Override 182 public void setTintMode(Mode tintMode) { 183 mState.mDrawable.setTintMode(tintMode); 184 } 185 186 @Override 187 public int getOpacity() { 188 return mState.mDrawable.getOpacity(); 189 } 190 191 @Override 192 public boolean canApplyTheme() { 193 return (mState != null && mState.canApplyTheme()) || super.canApplyTheme(); 194 } 195 196 @Override 197 public void invalidateDrawable(Drawable who) { 198 final Callback callback = getCallback(); 199 if (callback != null) { 200 callback.invalidateDrawable(this); 201 } 202 } 203 204 @Override 205 public void scheduleDrawable(Drawable who, Runnable what, long when) { 206 final Callback callback = getCallback(); 207 if (callback != null) { 208 callback.scheduleDrawable(this, what, when); 209 } 210 } 211 212 @Override 213 public void unscheduleDrawable(Drawable who, Runnable what) { 214 final Callback callback = getCallback(); 215 if (callback != null) { 216 callback.unscheduleDrawable(this, what); 217 } 218 } 219 220 @Override 221 public boolean getPadding(Rect padding) { 222 return mState.mDrawable.getPadding(padding); 223 } 224 225 @Override 226 public boolean isStateful() { 227 return mState.mDrawable.isStateful(); 228 } 229 230 @Override 231 protected void onBoundsChange(Rect bounds) { 232 mState.mDrawable.setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom); 233 } 234 235 @Override 236 protected boolean onLevelChange(int level) { 237 return mState.mDrawable.setLevel(level); 238 } 239 240 @Override 241 protected boolean onStateChange(int[] state) { 242 return mState.mDrawable.setState(state); 243 } 244 245 @Override 246 public int getIntrinsicWidth() { 247 return mState.mDrawable.getIntrinsicWidth(); 248 } 249 250 @Override 251 public int getIntrinsicHeight() { 252 return mState.mDrawable.getIntrinsicHeight(); 253 } 254 255 @Override 256 public ConstantState getConstantState() { 257 if (mState.canConstantState()) { 258 mState.mChangingConfigurations = getChangingConfigurations(); 259 return mState; 260 } 261 return null; 262 } 263 264 @Override 265 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 266 @NonNull AttributeSet attrs, @Nullable Theme theme) 267 throws XmlPullParserException, IOException { 268 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedRotateDrawable); 269 super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedRotateDrawable_visible); 270 updateStateFromTypedArray(a); 271 a.recycle(); 272 273 inflateChildElements(r, parser, attrs, theme); 274 275 init(); 276 } 277 278 @Override 279 public void applyTheme(@Nullable Theme t) { 280 super.applyTheme(t); 281 282 final AnimatedRotateState state = mState; 283 if (state == null) { 284 return; 285 } 286 287 if (state.mThemeAttrs != null) { 288 final TypedArray a = t.resolveAttributes( 289 state.mThemeAttrs, R.styleable.AnimatedRotateDrawable); 290 try { 291 updateStateFromTypedArray(a); 292 verifyRequiredAttributes(a); 293 } catch (XmlPullParserException e) { 294 throw new RuntimeException(e); 295 } finally { 296 a.recycle(); 297 } 298 } 299 300 if (state.mDrawable != null && state.mDrawable.canApplyTheme()) { 301 state.mDrawable.applyTheme(t); 302 } 303 304 init(); 305 } 306 307 private void updateStateFromTypedArray(TypedArray a) { 308 final AnimatedRotateState state = mState; 309 310 // Account for any configuration changes. 311 state.mChangingConfigurations |= a.getChangingConfigurations(); 312 313 // Extract the theme attributes, if any. 314 state.mThemeAttrs = a.extractThemeAttrs(); 315 316 if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotX)) { 317 final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotX); 318 state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION; 319 state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 320 } 321 322 if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotY)) { 323 final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotY); 324 state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION; 325 state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 326 } 327 328 setFramesCount(a.getInt( 329 R.styleable.AnimatedRotateDrawable_framesCount, state.mFramesCount)); 330 setFramesDuration(a.getInt( 331 R.styleable.AnimatedRotateDrawable_frameDuration, state.mFrameDuration)); 332 333 final Drawable dr = a.getDrawable(R.styleable.AnimatedRotateDrawable_drawable); 334 if (dr != null) { 335 state.mDrawable = dr; 336 dr.setCallback(this); 337 } 338 } 339 340 private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, 341 Theme theme) throws XmlPullParserException, IOException { 342 final AnimatedRotateState state = mState; 343 344 Drawable dr = null; 345 int outerDepth = parser.getDepth(); 346 int type; 347 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 348 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 349 if (type != XmlPullParser.START_TAG) { 350 continue; 351 } 352 353 if ((dr = Drawable.createFromXmlInner(r, parser, attrs, theme)) == null) { 354 Log.w(TAG, "Bad element under <animated-rotate>: " + parser.getName()); 355 } 356 } 357 358 if (dr != null) { 359 state.mDrawable = dr; 360 dr.setCallback(this); 361 } 362 } 363 364 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 365 // If we're not waiting on a theme, verify required attributes. 366 if (mState.mDrawable == null && (mState.mThemeAttrs == null 367 || mState.mThemeAttrs[R.styleable.AnimatedRotateDrawable_drawable] == 0)) { 368 throw new XmlPullParserException(a.getPositionDescription() 369 + ": <animated-rotate> tag requires a 'drawable' attribute or " 370 + "child tag defining a drawable"); 371 } 372 } 373 374 public void setFramesCount(int framesCount) { 375 mState.mFramesCount = framesCount; 376 mIncrement = 360.0f / mState.mFramesCount; 377 } 378 379 public void setFramesDuration(int framesDuration) { 380 mState.mFrameDuration = framesDuration; 381 } 382 383 @Override 384 public Drawable mutate() { 385 if (!mMutated && super.mutate() == this) { 386 mState.mDrawable.mutate(); 387 mMutated = true; 388 } 389 return this; 390 } 391 392 /** 393 * @hide 394 */ 395 public void clearMutated() { 396 super.clearMutated(); 397 mState.mDrawable.clearMutated(); 398 mMutated = false; 399 } 400 401 final static class AnimatedRotateState extends Drawable.ConstantState { 402 Drawable mDrawable; 403 int[] mThemeAttrs; 404 405 int mChangingConfigurations; 406 407 boolean mPivotXRel = false; 408 float mPivotX = 0; 409 boolean mPivotYRel = false; 410 float mPivotY = 0; 411 int mFrameDuration = 150; 412 int mFramesCount = 12; 413 414 private boolean mCanConstantState; 415 private boolean mCheckedConstantState; 416 417 public AnimatedRotateState(AnimatedRotateState orig, AnimatedRotateDrawable owner, 418 Resources res) { 419 if (orig != null) { 420 if (res != null) { 421 mDrawable = orig.mDrawable.getConstantState().newDrawable(res); 422 } else { 423 mDrawable = orig.mDrawable.getConstantState().newDrawable(); 424 } 425 mDrawable.setCallback(owner); 426 mDrawable.setLayoutDirection(orig.mDrawable.getLayoutDirection()); 427 mDrawable.setBounds(orig.mDrawable.getBounds()); 428 mDrawable.setLevel(orig.mDrawable.getLevel()); 429 mThemeAttrs = orig.mThemeAttrs; 430 mPivotXRel = orig.mPivotXRel; 431 mPivotX = orig.mPivotX; 432 mPivotYRel = orig.mPivotYRel; 433 mPivotY = orig.mPivotY; 434 mFramesCount = orig.mFramesCount; 435 mFrameDuration = orig.mFrameDuration; 436 mCanConstantState = mCheckedConstantState = true; 437 } 438 } 439 440 @Override 441 public Drawable newDrawable() { 442 return new AnimatedRotateDrawable(this, null); 443 } 444 445 @Override 446 public Drawable newDrawable(Resources res) { 447 return new AnimatedRotateDrawable(this, res); 448 } 449 450 @Override 451 public boolean canApplyTheme() { 452 return mThemeAttrs != null || (mDrawable != null && mDrawable.canApplyTheme()) 453 || super.canApplyTheme(); 454 } 455 456 @Override 457 public int getChangingConfigurations() { 458 return mChangingConfigurations; 459 } 460 461 public boolean canConstantState() { 462 if (!mCheckedConstantState) { 463 mCanConstantState = mDrawable.getConstantState() != null; 464 mCheckedConstantState = true; 465 } 466 467 return mCanConstantState; 468 } 469 } 470 } 471