RotateDrawable.java revision d03963ce364e9946f1bd603c2fcf07a49088af19
1/* 2 * Copyright (C) 2007 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 17package android.graphics.drawable; 18 19import com.android.internal.R; 20 21import org.xmlpull.v1.XmlPullParser; 22import org.xmlpull.v1.XmlPullParserException; 23 24import android.graphics.Canvas; 25import android.graphics.ColorFilter; 26import android.graphics.Rect; 27import android.graphics.PorterDuff.Mode; 28import android.content.res.ColorStateList; 29import android.content.res.Resources; 30import android.content.res.TypedArray; 31import android.content.res.Resources.Theme; 32import android.util.TypedValue; 33import android.util.AttributeSet; 34 35import java.io.IOException; 36 37/** 38 * <p> 39 * A Drawable that can rotate another Drawable based on the current level value. 40 * The start and end angles of rotation can be controlled to map any circular 41 * arc to the level values range. 42 * <p> 43 * It can be defined in an XML file with the <code><rotate></code> element. 44 * For more information, see the guide to 45 * <a href="{@docRoot}guide/topics/resources/animation-resource.html">Animation Resources</a>. 46 * 47 * @attr ref android.R.styleable#RotateDrawable_visible 48 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 49 * @attr ref android.R.styleable#RotateDrawable_toDegrees 50 * @attr ref android.R.styleable#RotateDrawable_pivotX 51 * @attr ref android.R.styleable#RotateDrawable_pivotY 52 * @attr ref android.R.styleable#RotateDrawable_drawable 53 */ 54public class RotateDrawable extends Drawable implements Drawable.Callback { 55 private static final float MAX_LEVEL = 10000.0f; 56 57 private final RotateState mState; 58 59 private boolean mMutated; 60 61 /** 62 * Create a new rotating drawable with an empty state. 63 */ 64 public RotateDrawable() { 65 this(null, null); 66 } 67 68 /** 69 * Create a new rotating drawable with the specified state. A copy of 70 * this state is used as the internal state for the newly created 71 * drawable. 72 * 73 * @param rotateState the state for this drawable 74 */ 75 private RotateDrawable(RotateState rotateState, Resources res) { 76 mState = new RotateState(rotateState, this, res); 77 } 78 79 @Override 80 public void draw(Canvas canvas) { 81 final RotateState st = mState; 82 final Drawable d = st.mDrawable; 83 final Rect bounds = d.getBounds(); 84 final int w = bounds.right - bounds.left; 85 final int h = bounds.bottom - bounds.top; 86 final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX; 87 final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY; 88 89 final int saveCount = canvas.save(); 90 canvas.rotate(st.mCurrentDegrees, px + bounds.left, py + bounds.top); 91 d.draw(canvas); 92 canvas.restoreToCount(saveCount); 93 } 94 95 /** 96 * Sets the drawable rotated by this RotateDrawable. 97 * 98 * @param drawable The drawable to rotate 99 */ 100 public void setDrawable(Drawable drawable) { 101 final Drawable oldDrawable = mState.mDrawable; 102 if (oldDrawable != drawable) { 103 if (oldDrawable != null) { 104 oldDrawable.setCallback(null); 105 } 106 mState.mDrawable = drawable; 107 if (drawable != null) { 108 drawable.setCallback(this); 109 } 110 } 111 } 112 113 /** 114 * @return The drawable rotated by this RotateDrawable 115 */ 116 public Drawable getDrawable() { 117 return mState.mDrawable; 118 } 119 120 @Override 121 public int getChangingConfigurations() { 122 return super.getChangingConfigurations() 123 | mState.mChangingConfigurations 124 | mState.mDrawable.getChangingConfigurations(); 125 } 126 127 @Override 128 public void setAlpha(int alpha) { 129 mState.mDrawable.setAlpha(alpha); 130 } 131 132 @Override 133 public int getAlpha() { 134 return mState.mDrawable.getAlpha(); 135 } 136 137 @Override 138 public void setColorFilter(ColorFilter cf) { 139 mState.mDrawable.setColorFilter(cf); 140 } 141 142 @Override 143 public void setTintList(ColorStateList tint) { 144 mState.mDrawable.setTintList(tint); 145 } 146 147 @Override 148 public void setTintMode(Mode tintMode) { 149 mState.mDrawable.setTintMode(tintMode); 150 } 151 152 @Override 153 public int getOpacity() { 154 return mState.mDrawable.getOpacity(); 155 } 156 157 /** 158 * Sets the start angle for rotation. 159 * 160 * @param fromDegrees Starting angle in degrees 161 * 162 * @see #getFromDegrees() 163 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 164 */ 165 public void setFromDegrees(float fromDegrees) { 166 if (mState.mFromDegrees != fromDegrees) { 167 mState.mFromDegrees = fromDegrees; 168 invalidateSelf(); 169 } 170 } 171 172 /** 173 * @return The starting angle for rotation in degrees 174 * 175 * @see #setFromDegrees(float) 176 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 177 */ 178 public float getFromDegrees() { 179 return mState.mFromDegrees; 180 } 181 182 /** 183 * Sets the end angle for rotation. 184 * 185 * @param toDegrees Ending angle in degrees 186 * 187 * @see #getToDegrees() 188 * @attr ref android.R.styleable#RotateDrawable_toDegrees 189 */ 190 public void setToDegrees(float toDegrees) { 191 if (mState.mToDegrees != toDegrees) { 192 mState.mToDegrees = toDegrees; 193 invalidateSelf(); 194 } 195 } 196 197 /** 198 * @return The ending angle for rotation in degrees 199 * 200 * @see #setToDegrees(float) 201 * @attr ref android.R.styleable#RotateDrawable_toDegrees 202 */ 203 public float getToDegrees() { 204 return mState.mToDegrees; 205 } 206 207 /** 208 * Sets the X position around which the drawable is rotated. 209 * 210 * @param pivotX X position around which to rotate. If the X pivot is 211 * relative, the position represents a fraction of the drawable 212 * width. Otherwise, the position represents an absolute value in 213 * pixels. 214 * 215 * @see #setPivotXRelative(boolean) 216 * @attr ref android.R.styleable#RotateDrawable_pivotX 217 */ 218 public void setPivotX(float pivotX) { 219 if (mState.mPivotX == pivotX) { 220 mState.mPivotX = pivotX; 221 invalidateSelf(); 222 } 223 } 224 225 /** 226 * @return X position around which to rotate 227 * 228 * @see #setPivotX(float) 229 * @attr ref android.R.styleable#RotateDrawable_pivotX 230 */ 231 public float getPivotX() { 232 return mState.mPivotX; 233 } 234 235 /** 236 * Sets whether the X pivot value represents a fraction of the drawable 237 * width or an absolute value in pixels. 238 * 239 * @param relative True if the X pivot represents a fraction of the drawable 240 * width, or false if it represents an absolute value in pixels 241 * 242 * @see #isPivotXRelative() 243 */ 244 public void setPivotXRelative(boolean relative) { 245 if (mState.mPivotXRel == relative) { 246 mState.mPivotXRel = relative; 247 invalidateSelf(); 248 } 249 } 250 251 /** 252 * @return True if the X pivot represents a fraction of the drawable width, 253 * or false if it represents an absolute value in pixels 254 * 255 * @see #setPivotXRelative(boolean) 256 */ 257 public boolean isPivotXRelative() { 258 return mState.mPivotXRel; 259 } 260 261 /** 262 * Sets the Y position around which the drawable is rotated. 263 * 264 * @param pivotY Y position around which to rotate. If the Y pivot is 265 * relative, the position represents a fraction of the drawable 266 * height. Otherwise, the position represents an absolute value 267 * in pixels. 268 * 269 * @see #getPivotY() 270 * @attr ref android.R.styleable#RotateDrawable_pivotY 271 */ 272 public void setPivotY(float pivotY) { 273 if (mState.mPivotY == pivotY) { 274 mState.mPivotY = pivotY; 275 invalidateSelf(); 276 } 277 } 278 279 /** 280 * @return Y position around which to rotate 281 * 282 * @see #setPivotY(float) 283 * @attr ref android.R.styleable#RotateDrawable_pivotY 284 */ 285 public float getPivotY() { 286 return mState.mPivotY; 287 } 288 289 /** 290 * Sets whether the Y pivot value represents a fraction of the drawable 291 * height or an absolute value in pixels. 292 * 293 * @param relative True if the Y pivot represents a fraction of the drawable 294 * height, or false if it represents an absolute value in pixels 295 * 296 * @see #isPivotYRelative() 297 */ 298 public void setPivotYRelative(boolean relative) { 299 if (mState.mPivotYRel == relative) { 300 mState.mPivotYRel = relative; 301 invalidateSelf(); 302 } 303 } 304 305 /** 306 * @return True if the Y pivot represents a fraction of the drawable height, 307 * or false if it represents an absolute value in pixels 308 * 309 * @see #setPivotYRelative(boolean) 310 */ 311 public boolean isPivotYRelative() { 312 return mState.mPivotYRel; 313 } 314 315 @Override 316 public boolean canApplyTheme() { 317 return (mState != null && mState.canApplyTheme()) || super.canApplyTheme(); 318 } 319 320 @Override 321 public void invalidateDrawable(Drawable who) { 322 final Callback callback = getCallback(); 323 if (callback != null) { 324 callback.invalidateDrawable(this); 325 } 326 } 327 328 @Override 329 public void scheduleDrawable(Drawable who, Runnable what, long when) { 330 final Callback callback = getCallback(); 331 if (callback != null) { 332 callback.scheduleDrawable(this, what, when); 333 } 334 } 335 336 @Override 337 public void unscheduleDrawable(Drawable who, Runnable what) { 338 final Callback callback = getCallback(); 339 if (callback != null) { 340 callback.unscheduleDrawable(this, what); 341 } 342 } 343 344 @Override 345 public boolean getPadding(Rect padding) { 346 return mState.mDrawable.getPadding(padding); 347 } 348 349 @Override 350 public boolean setVisible(boolean visible, boolean restart) { 351 mState.mDrawable.setVisible(visible, restart); 352 return super.setVisible(visible, restart); 353 } 354 355 @Override 356 public boolean isStateful() { 357 return mState.mDrawable.isStateful(); 358 } 359 360 @Override 361 protected boolean onStateChange(int[] state) { 362 final boolean changed = mState.mDrawable.setState(state); 363 onBoundsChange(getBounds()); 364 return changed; 365 } 366 367 @Override 368 protected boolean onLevelChange(int level) { 369 mState.mDrawable.setLevel(level); 370 onBoundsChange(getBounds()); 371 372 mState.mCurrentDegrees = mState.mFromDegrees + 373 (mState.mToDegrees - mState.mFromDegrees) * 374 (level / MAX_LEVEL); 375 376 invalidateSelf(); 377 return true; 378 } 379 380 @Override 381 protected void onBoundsChange(Rect bounds) { 382 mState.mDrawable.setBounds(bounds.left, bounds.top, 383 bounds.right, bounds.bottom); 384 } 385 386 @Override 387 public int getIntrinsicWidth() { 388 return mState.mDrawable.getIntrinsicWidth(); 389 } 390 391 @Override 392 public int getIntrinsicHeight() { 393 return mState.mDrawable.getIntrinsicHeight(); 394 } 395 396 @Override 397 public ConstantState getConstantState() { 398 if (mState.canConstantState()) { 399 mState.mChangingConfigurations = getChangingConfigurations(); 400 return mState; 401 } 402 return null; 403 } 404 405 @Override 406 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 407 throws XmlPullParserException, IOException { 408 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RotateDrawable); 409 super.inflateWithAttributes(r, parser, a, R.styleable.RotateDrawable_visible); 410 411 // Reset mDrawable to preserve old multiple-inflate behavior. This is 412 // silly, but we have CTS tests that rely on it. 413 mState.mDrawable = null; 414 415 updateStateFromTypedArray(a); 416 inflateChildElements(r, parser, attrs, theme); 417 verifyRequiredAttributes(a); 418 a.recycle(); 419 } 420 421 @Override 422 public void applyTheme(Theme t) { 423 super.applyTheme(t); 424 425 final RotateState state = mState; 426 if (state == null) { 427 return; 428 } 429 430 if (state.mThemeAttrs != null) { 431 final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.RotateDrawable); 432 try { 433 updateStateFromTypedArray(a); 434 verifyRequiredAttributes(a); 435 } catch (XmlPullParserException e) { 436 throw new RuntimeException(e); 437 } finally { 438 a.recycle(); 439 } 440 } 441 442 if (state.mDrawable != null && state.mDrawable.canApplyTheme()) { 443 state.mDrawable.applyTheme(t); 444 } 445 446 } 447 448 private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, 449 Theme theme) throws XmlPullParserException, IOException { 450 Drawable dr = null; 451 int type; 452 final int outerDepth = parser.getDepth(); 453 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 454 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 455 if (type != XmlPullParser.START_TAG) { 456 continue; 457 } 458 dr = Drawable.createFromXmlInner(r, parser, attrs, theme); 459 } 460 461 if (dr != null) { 462 mState.mDrawable = dr; 463 dr.setCallback(this); 464 } 465 } 466 467 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 468 // If we're not waiting on a theme, verify required attributes. 469 if (mState.mDrawable == null && (mState.mThemeAttrs == null 470 || mState.mThemeAttrs[R.styleable.ScaleDrawable_drawable] == 0)) { 471 throw new XmlPullParserException(a.getPositionDescription() 472 + ": <rotate> tag requires a 'drawable' attribute or " 473 + "child tag defining a drawable"); 474 } 475 } 476 477 private void updateStateFromTypedArray(TypedArray a) { 478 final RotateState state = mState; 479 480 // Account for any configuration changes. 481 state.mChangingConfigurations |= a.getChangingConfigurations(); 482 483 // Extract the theme attributes, if any. 484 state.mThemeAttrs = a.extractThemeAttrs(); 485 486 if (a.hasValue(R.styleable.RotateDrawable_pivotX)) { 487 final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotX); 488 state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION; 489 state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 490 } 491 492 if (a.hasValue(R.styleable.RotateDrawable_pivotY)) { 493 final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotY); 494 state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION; 495 state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 496 } 497 498 state.mFromDegrees = a.getFloat(R.styleable.RotateDrawable_fromDegrees, state.mFromDegrees); 499 state.mToDegrees = a.getFloat(R.styleable.RotateDrawable_toDegrees, state.mToDegrees); 500 state.mCurrentDegrees = state.mFromDegrees; 501 502 final Drawable dr = a.getDrawable(R.styleable.RotateDrawable_drawable); 503 if (dr != null) { 504 state.mDrawable = dr; 505 dr.setCallback(this); 506 } 507 } 508 509 @Override 510 public Drawable mutate() { 511 if (!mMutated && super.mutate() == this) { 512 mState.mDrawable.mutate(); 513 mMutated = true; 514 } 515 return this; 516 } 517 518 /** 519 * @hide 520 */ 521 public void clearMutated() { 522 super.clearMutated(); 523 mState.mDrawable.clearMutated(); 524 mMutated = false; 525 } 526 527 /** 528 * Represents the state of a rotation for a given drawable. The same 529 * rotate drawable can be invoked with different states to drive several 530 * rotations at the same time. 531 */ 532 final static class RotateState extends Drawable.ConstantState { 533 int[] mThemeAttrs; 534 int mChangingConfigurations; 535 536 Drawable mDrawable; 537 538 boolean mPivotXRel = true; 539 float mPivotX = 0.5f; 540 boolean mPivotYRel = true; 541 float mPivotY = 0.5f; 542 543 float mFromDegrees = 0.0f; 544 float mToDegrees = 360.0f; 545 546 float mCurrentDegrees = 0.0f; 547 548 private boolean mCheckedConstantState; 549 private boolean mCanConstantState; 550 551 RotateState(RotateState orig, RotateDrawable owner, Resources res) { 552 if (orig != null) { 553 mThemeAttrs = orig.mThemeAttrs; 554 mChangingConfigurations = orig.mChangingConfigurations; 555 if (res != null) { 556 mDrawable = orig.mDrawable.getConstantState().newDrawable(res); 557 } else { 558 mDrawable = orig.mDrawable.getConstantState().newDrawable(); 559 } 560 mDrawable.setCallback(owner); 561 mDrawable.setLayoutDirection(orig.mDrawable.getLayoutDirection()); 562 mDrawable.setBounds(orig.mDrawable.getBounds()); 563 mDrawable.setLevel(orig.mDrawable.getLevel()); 564 mPivotXRel = orig.mPivotXRel; 565 mPivotX = orig.mPivotX; 566 mPivotYRel = orig.mPivotYRel; 567 mPivotY = orig.mPivotY; 568 mFromDegrees = orig.mFromDegrees; 569 mToDegrees = orig.mToDegrees; 570 mCurrentDegrees = orig.mCurrentDegrees; 571 mCheckedConstantState = mCanConstantState = true; 572 } 573 } 574 575 @Override 576 public boolean canApplyTheme() { 577 return mThemeAttrs != null || (mDrawable != null && mDrawable.canApplyTheme()) 578 || super.canApplyTheme(); 579 } 580 581 @Override 582 public Drawable newDrawable() { 583 return new RotateDrawable(this, null); 584 } 585 586 @Override 587 public Drawable newDrawable(Resources res) { 588 return new RotateDrawable(this, res); 589 } 590 591 @Override 592 public int getChangingConfigurations() { 593 return mChangingConfigurations; 594 } 595 596 public boolean canConstantState() { 597 if (!mCheckedConstantState) { 598 mCanConstantState = mDrawable.getConstantState() != null; 599 mCheckedConstantState = true; 600 } 601 602 return mCanConstantState; 603 } 604 } 605} 606