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