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