RotateDrawable.java revision b3c56086d802ae28888dd97ba1f49bd6cee0b673
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 setTint(ColorStateList tint, Mode tintMode) { 143 mState.mDrawable.setTint(tint, tintMode); 144 } 145 146 @Override 147 public int getOpacity() { 148 return mState.mDrawable.getOpacity(); 149 } 150 151 /** 152 * Sets the start angle for rotation. 153 * 154 * @param fromDegrees Starting angle in degrees 155 * 156 * @see #getFromDegrees() 157 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 158 */ 159 public void setFromDegrees(float fromDegrees) { 160 if (mState.mFromDegrees != fromDegrees) { 161 mState.mFromDegrees = fromDegrees; 162 invalidateSelf(); 163 } 164 } 165 166 /** 167 * @return The starting angle for rotation in degrees 168 * 169 * @see #setFromDegrees(float) 170 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 171 */ 172 public float getFromDegrees() { 173 return mState.mFromDegrees; 174 } 175 176 /** 177 * Sets the end angle for rotation. 178 * 179 * @param toDegrees Ending angle in degrees 180 * 181 * @see #getToDegrees() 182 * @attr ref android.R.styleable#RotateDrawable_toDegrees 183 */ 184 public void setToDegrees(float toDegrees) { 185 if (mState.mToDegrees != toDegrees) { 186 mState.mToDegrees = toDegrees; 187 invalidateSelf(); 188 } 189 } 190 191 /** 192 * @return The ending angle for rotation in degrees 193 * 194 * @see #setToDegrees(float) 195 * @attr ref android.R.styleable#RotateDrawable_toDegrees 196 */ 197 public float getToDegrees() { 198 return mState.mToDegrees; 199 } 200 201 /** 202 * Sets the X position around which the drawable is rotated. 203 * 204 * @param pivotX X position around which to rotate. If the X pivot is 205 * relative, the position represents a fraction of the drawable 206 * width. Otherwise, the position represents an absolute value in 207 * pixels. 208 * 209 * @see #setPivotXRelative(boolean) 210 * @attr ref android.R.styleable#RotateDrawable_pivotX 211 */ 212 public void setPivotX(float pivotX) { 213 if (mState.mPivotX == pivotX) { 214 mState.mPivotX = pivotX; 215 invalidateSelf(); 216 } 217 } 218 219 /** 220 * @return X position around which to rotate 221 * 222 * @see #setPivotX(float) 223 * @attr ref android.R.styleable#RotateDrawable_pivotX 224 */ 225 public float getPivotX() { 226 return mState.mPivotX; 227 } 228 229 /** 230 * Sets whether the X pivot value represents a fraction of the drawable 231 * width or an absolute value in pixels. 232 * 233 * @param relative True if the X pivot represents a fraction of the drawable 234 * width, or false if it represents an absolute value in pixels 235 * 236 * @see #isPivotXRelative() 237 */ 238 public void setPivotXRelative(boolean relative) { 239 if (mState.mPivotXRel == relative) { 240 mState.mPivotXRel = relative; 241 invalidateSelf(); 242 } 243 } 244 245 /** 246 * @return True if the X pivot represents a fraction of the drawable width, 247 * or false if it represents an absolute value in pixels 248 * 249 * @see #setPivotXRelative(boolean) 250 */ 251 public boolean isPivotXRelative() { 252 return mState.mPivotXRel; 253 } 254 255 /** 256 * Sets the Y position around which the drawable is rotated. 257 * 258 * @param pivotY Y position around which to rotate. If the Y pivot is 259 * relative, the position represents a fraction of the drawable 260 * height. Otherwise, the position represents an absolute value 261 * in pixels. 262 * 263 * @see #getPivotY() 264 * @attr ref android.R.styleable#RotateDrawable_pivotY 265 */ 266 public void setPivotY(float pivotY) { 267 if (mState.mPivotY == pivotY) { 268 mState.mPivotY = pivotY; 269 invalidateSelf(); 270 } 271 } 272 273 /** 274 * @return Y position around which to rotate 275 * 276 * @see #setPivotY(float) 277 * @attr ref android.R.styleable#RotateDrawable_pivotY 278 */ 279 public float getPivotY() { 280 return mState.mPivotY; 281 } 282 283 /** 284 * Sets whether the Y pivot value represents a fraction of the drawable 285 * height or an absolute value in pixels. 286 * 287 * @param relative True if the Y pivot represents a fraction of the drawable 288 * height, or false if it represents an absolute value in pixels 289 * 290 * @see #isPivotYRelative() 291 */ 292 public void setPivotYRelative(boolean relative) { 293 if (mState.mPivotYRel == relative) { 294 mState.mPivotYRel = relative; 295 invalidateSelf(); 296 } 297 } 298 299 /** 300 * @return True if the Y pivot represents a fraction of the drawable height, 301 * or false if it represents an absolute value in pixels 302 * 303 * @see #setPivotYRelative(boolean) 304 */ 305 public boolean isPivotYRelative() { 306 return mState.mPivotYRel; 307 } 308 309 @Override 310 public void invalidateDrawable(Drawable who) { 311 final Callback callback = getCallback(); 312 if (callback != null) { 313 callback.invalidateDrawable(this); 314 } 315 } 316 317 @Override 318 public void scheduleDrawable(Drawable who, Runnable what, long when) { 319 final Callback callback = getCallback(); 320 if (callback != null) { 321 callback.scheduleDrawable(this, what, when); 322 } 323 } 324 325 @Override 326 public void unscheduleDrawable(Drawable who, Runnable what) { 327 final Callback callback = getCallback(); 328 if (callback != null) { 329 callback.unscheduleDrawable(this, what); 330 } 331 } 332 333 @Override 334 public boolean getPadding(Rect padding) { 335 return mState.mDrawable.getPadding(padding); 336 } 337 338 @Override 339 public boolean setVisible(boolean visible, boolean restart) { 340 mState.mDrawable.setVisible(visible, restart); 341 return super.setVisible(visible, restart); 342 } 343 344 @Override 345 public boolean isStateful() { 346 return mState.mDrawable.isStateful(); 347 } 348 349 @Override 350 protected boolean onStateChange(int[] state) { 351 final boolean changed = mState.mDrawable.setState(state); 352 onBoundsChange(getBounds()); 353 return changed; 354 } 355 356 @Override 357 protected boolean onLevelChange(int level) { 358 mState.mDrawable.setLevel(level); 359 onBoundsChange(getBounds()); 360 361 mState.mCurrentDegrees = mState.mFromDegrees + 362 (mState.mToDegrees - mState.mFromDegrees) * 363 (level / MAX_LEVEL); 364 365 invalidateSelf(); 366 return true; 367 } 368 369 @Override 370 protected void onBoundsChange(Rect bounds) { 371 mState.mDrawable.setBounds(bounds.left, bounds.top, 372 bounds.right, bounds.bottom); 373 } 374 375 @Override 376 public int getIntrinsicWidth() { 377 return mState.mDrawable.getIntrinsicWidth(); 378 } 379 380 @Override 381 public int getIntrinsicHeight() { 382 return mState.mDrawable.getIntrinsicHeight(); 383 } 384 385 @Override 386 public ConstantState getConstantState() { 387 if (mState.canConstantState()) { 388 mState.mChangingConfigurations = getChangingConfigurations(); 389 return mState; 390 } 391 return null; 392 } 393 394 @Override 395 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 396 throws XmlPullParserException, IOException { 397 final TypedArray a = r.obtainAttributes(attrs, 398 com.android.internal.R.styleable.RotateDrawable); 399 400 super.inflateWithAttributes(r, parser, a, 401 com.android.internal.R.styleable.RotateDrawable_visible); 402 403 TypedValue tv = a.peekValue(com.android.internal.R.styleable.RotateDrawable_pivotX); 404 final boolean pivotXRel; 405 final float pivotX; 406 if (tv == null) { 407 pivotXRel = true; 408 pivotX = 0.5f; 409 } else { 410 pivotXRel = tv.type == TypedValue.TYPE_FRACTION; 411 pivotX = pivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 412 } 413 414 tv = a.peekValue(com.android.internal.R.styleable.RotateDrawable_pivotY); 415 final boolean pivotYRel; 416 final float pivotY; 417 if (tv == null) { 418 pivotYRel = true; 419 pivotY = 0.5f; 420 } else { 421 pivotYRel = tv.type == TypedValue.TYPE_FRACTION; 422 pivotY = pivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 423 } 424 425 final float fromDegrees = a.getFloat( 426 com.android.internal.R.styleable.RotateDrawable_fromDegrees, 0.0f); 427 final float toDegrees = a.getFloat( 428 com.android.internal.R.styleable.RotateDrawable_toDegrees, 360.0f); 429 430 final int res = a.getResourceId( 431 com.android.internal.R.styleable.RotateDrawable_drawable, 0); 432 Drawable drawable = null; 433 if (res > 0) { 434 drawable = r.getDrawable(res); 435 } 436 437 a.recycle(); 438 439 final int outerDepth = parser.getDepth(); 440 int type; 441 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && 442 (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 443 444 if (type != XmlPullParser.START_TAG) { 445 continue; 446 } 447 448 if ((drawable = Drawable.createFromXmlInner(r, parser, attrs, theme)) == null) { 449 Log.w("drawable", "Bad element under <rotate>: " 450 + parser .getName()); 451 } 452 } 453 454 if (drawable == null) { 455 Log.w("drawable", "No drawable specified for <rotate>"); 456 } 457 458 final RotateState st = mState; 459 st.mDrawable = drawable; 460 st.mPivotXRel = pivotXRel; 461 st.mPivotX = pivotX; 462 st.mPivotYRel = pivotYRel; 463 st.mPivotY = pivotY; 464 st.mFromDegrees = fromDegrees; 465 st.mCurrentDegrees = fromDegrees; 466 st.mToDegrees = toDegrees; 467 468 if (drawable != null) { 469 drawable.setCallback(this); 470 } 471 } 472 473 @Override 474 public Drawable mutate() { 475 if (!mMutated && super.mutate() == this) { 476 mState.mDrawable.mutate(); 477 mMutated = true; 478 } 479 return this; 480 } 481 482 /** 483 * Represents the state of a rotation for a given drawable. The same 484 * rotate drawable can be invoked with different states to drive several 485 * rotations at the same time. 486 */ 487 final static class RotateState extends Drawable.ConstantState { 488 Drawable mDrawable; 489 490 int mChangingConfigurations; 491 492 boolean mPivotXRel; 493 float mPivotX; 494 boolean mPivotYRel; 495 float mPivotY; 496 497 float mFromDegrees; 498 float mToDegrees; 499 500 float mCurrentDegrees; 501 502 private boolean mCanConstantState; 503 private boolean mCheckedConstantState; 504 505 public RotateState(RotateState source, RotateDrawable owner, Resources res) { 506 if (source != null) { 507 if (res != null) { 508 mDrawable = source.mDrawable.getConstantState().newDrawable(res); 509 } else { 510 mDrawable = source.mDrawable.getConstantState().newDrawable(); 511 } 512 mDrawable.setCallback(owner); 513 mDrawable.setLayoutDirection(source.mDrawable.getLayoutDirection()); 514 mPivotXRel = source.mPivotXRel; 515 mPivotX = source.mPivotX; 516 mPivotYRel = source.mPivotYRel; 517 mPivotY = source.mPivotY; 518 mFromDegrees = mCurrentDegrees = source.mFromDegrees; 519 mToDegrees = source.mToDegrees; 520 mCanConstantState = mCheckedConstantState = true; 521 } 522 } 523 524 @Override 525 public Drawable newDrawable() { 526 return new RotateDrawable(this, null); 527 } 528 529 @Override 530 public Drawable newDrawable(Resources res) { 531 return new RotateDrawable(this, res); 532 } 533 534 @Override 535 public int getChangingConfigurations() { 536 return mChangingConfigurations; 537 } 538 539 public boolean canConstantState() { 540 if (!mCheckedConstantState) { 541 mCanConstantState = mDrawable.getConstantState() != null; 542 mCheckedConstantState = true; 543 } 544 545 return mCanConstantState; 546 } 547 } 548} 549