RotateDrawable.java revision 039bd51a23d886d0acaa93458e286329503bd243
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.MathUtils; 33import android.util.TypedValue; 34import android.util.AttributeSet; 35 36import java.io.IOException; 37 38/** 39 * <p> 40 * A Drawable that can rotate another Drawable based on the current level value. 41 * The start and end angles of rotation can be controlled to map any circular 42 * arc to the level values range. 43 * <p> 44 * It can be defined in an XML file with the <code><rotate></code> element. 45 * For more information, see the guide to 46 * <a href="{@docRoot}guide/topics/resources/animation-resource.html">Animation Resources</a>. 47 * 48 * @attr ref android.R.styleable#RotateDrawable_visible 49 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 50 * @attr ref android.R.styleable#RotateDrawable_toDegrees 51 * @attr ref android.R.styleable#RotateDrawable_pivotX 52 * @attr ref android.R.styleable#RotateDrawable_pivotY 53 * @attr ref android.R.styleable#RotateDrawable_drawable 54 */ 55public class RotateDrawable extends DrawableWrapper { 56 private static final int MAX_LEVEL = 10000; 57 58 private RotateState mState; 59 60 /** 61 * Creates a new rotating drawable with no wrapped drawable. 62 */ 63 public RotateDrawable() { 64 this(null, null); 65 } 66 67 @Override 68 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 69 throws XmlPullParserException, IOException { 70 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RotateDrawable); 71 super.inflateWithAttributes(r, parser, a, R.styleable.RotateDrawable_visible); 72 73 updateStateFromTypedArray(a); 74 inflateChildDrawable(r, parser, attrs, theme); 75 verifyRequiredAttributes(a); 76 a.recycle(); 77 } 78 79 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 80 // If we're not waiting on a theme, verify required attributes. 81 if (getDrawable() == null && (mState.mThemeAttrs == null 82 || mState.mThemeAttrs[R.styleable.RotateDrawable_drawable] == 0)) { 83 throw new XmlPullParserException(a.getPositionDescription() 84 + ": <rotate> tag requires a 'drawable' attribute or " 85 + "child tag defining a drawable"); 86 } 87 } 88 89 @Override 90 void updateStateFromTypedArray(TypedArray a) { 91 super.updateStateFromTypedArray(a); 92 93 final RotateState state = mState; 94 95 // Account for any configuration changes. 96 state.mChangingConfigurations |= a.getChangingConfigurations(); 97 98 // Extract the theme attributes, if any. 99 state.mThemeAttrs = a.extractThemeAttrs(); 100 101 if (a.hasValue(R.styleable.RotateDrawable_pivotX)) { 102 final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotX); 103 state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION; 104 state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 105 } 106 107 if (a.hasValue(R.styleable.RotateDrawable_pivotY)) { 108 final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotY); 109 state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION; 110 state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 111 } 112 113 state.mFromDegrees = a.getFloat( 114 R.styleable.RotateDrawable_fromDegrees, state.mFromDegrees); 115 state.mToDegrees = a.getFloat( 116 R.styleable.RotateDrawable_toDegrees, state.mToDegrees); 117 state.mCurrentDegrees = state.mFromDegrees; 118 119 final Drawable dr = a.getDrawable(R.styleable.RotateDrawable_drawable); 120 if (dr != null) { 121 setDrawable(dr); 122 } 123 } 124 125 @Override 126 public void applyTheme(Theme t) { 127 final RotateState state = mState; 128 if (state == null) { 129 return; 130 } 131 132 if (state.mThemeAttrs != null) { 133 final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.RotateDrawable); 134 try { 135 updateStateFromTypedArray(a); 136 verifyRequiredAttributes(a); 137 } catch (XmlPullParserException e) { 138 throw new RuntimeException(e); 139 } finally { 140 a.recycle(); 141 } 142 } 143 144 // The drawable may have changed as a result of applying the theme, so 145 // apply the theme to the wrapped drawable last. 146 super.applyTheme(t); 147 } 148 149 @Override 150 public void draw(Canvas canvas) { 151 final Drawable d = getDrawable(); 152 final Rect bounds = d.getBounds(); 153 final int w = bounds.right - bounds.left; 154 final int h = bounds.bottom - bounds.top; 155 final RotateState st = mState; 156 final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX; 157 final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY; 158 159 final int saveCount = canvas.save(); 160 canvas.rotate(st.mCurrentDegrees, px + bounds.left, py + bounds.top); 161 d.draw(canvas); 162 canvas.restoreToCount(saveCount); 163 } 164 165 /** 166 * Sets the start angle for rotation. 167 * 168 * @param fromDegrees starting angle in degrees 169 * @see #getFromDegrees() 170 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 171 */ 172 public void setFromDegrees(float fromDegrees) { 173 if (mState.mFromDegrees != fromDegrees) { 174 mState.mFromDegrees = fromDegrees; 175 invalidateSelf(); 176 } 177 } 178 179 /** 180 * @return starting angle for rotation in degrees 181 * @see #setFromDegrees(float) 182 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 183 */ 184 public float getFromDegrees() { 185 return mState.mFromDegrees; 186 } 187 188 /** 189 * Sets the end angle for rotation. 190 * 191 * @param toDegrees ending angle in degrees 192 * @see #getToDegrees() 193 * @attr ref android.R.styleable#RotateDrawable_toDegrees 194 */ 195 public void setToDegrees(float toDegrees) { 196 if (mState.mToDegrees != toDegrees) { 197 mState.mToDegrees = toDegrees; 198 invalidateSelf(); 199 } 200 } 201 202 /** 203 * @return ending angle for rotation in degrees 204 * @see #setToDegrees(float) 205 * @attr ref android.R.styleable#RotateDrawable_toDegrees 206 */ 207 public float getToDegrees() { 208 return mState.mToDegrees; 209 } 210 211 /** 212 * Sets the X position around which the drawable is rotated. 213 * 214 * @param pivotX X position around which to rotate. If the X pivot is 215 * relative, the position represents a fraction of the drawable 216 * width. Otherwise, the position represents an absolute value in 217 * pixels. 218 * @see #setPivotXRelative(boolean) 219 * @attr ref android.R.styleable#RotateDrawable_pivotX 220 */ 221 public void setPivotX(float pivotX) { 222 if (mState.mPivotX != pivotX) { 223 mState.mPivotX = pivotX; 224 invalidateSelf(); 225 } 226 } 227 228 /** 229 * @return X position around which to rotate 230 * @see #setPivotX(float) 231 * @attr ref android.R.styleable#RotateDrawable_pivotX 232 */ 233 public float getPivotX() { 234 return mState.mPivotX; 235 } 236 237 /** 238 * Sets whether the X pivot value represents a fraction of the drawable 239 * width or an absolute value in pixels. 240 * 241 * @param relative true if the X pivot represents a fraction of the drawable 242 * width, or false if it represents an absolute value in pixels 243 * @see #isPivotXRelative() 244 */ 245 public void setPivotXRelative(boolean relative) { 246 if (mState.mPivotXRel != relative) { 247 mState.mPivotXRel = relative; 248 invalidateSelf(); 249 } 250 } 251 252 /** 253 * @return true if the X pivot represents a fraction of the drawable width, 254 * or false if it represents an absolute value in pixels 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 * @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 * @see #setPivotY(float) 281 * @attr ref android.R.styleable#RotateDrawable_pivotY 282 */ 283 public float getPivotY() { 284 return mState.mPivotY; 285 } 286 287 /** 288 * Sets whether the Y pivot value represents a fraction of the drawable 289 * height or an absolute value in pixels. 290 * 291 * @param relative True if the Y pivot represents a fraction of the drawable 292 * height, or false if it represents an absolute value in pixels 293 * @see #isPivotYRelative() 294 */ 295 public void setPivotYRelative(boolean relative) { 296 if (mState.mPivotYRel != relative) { 297 mState.mPivotYRel = relative; 298 invalidateSelf(); 299 } 300 } 301 302 /** 303 * @return true if the Y pivot represents a fraction of the drawable height, 304 * or false if it represents an absolute value in pixels 305 * @see #setPivotYRelative(boolean) 306 */ 307 public boolean isPivotYRelative() { 308 return mState.mPivotYRel; 309 } 310 311 @Override 312 protected boolean onLevelChange(int level) { 313 super.onLevelChange(level); 314 315 final float value = level / (float) MAX_LEVEL; 316 final float degrees = MathUtils.lerp(mState.mFromDegrees, mState.mToDegrees, value); 317 mState.mCurrentDegrees = degrees; 318 319 invalidateSelf(); 320 return true; 321 } 322 323 @Override 324 DrawableWrapperState mutateConstantState() { 325 mState = new RotateState(mState); 326 return mState; 327 } 328 329 static final class RotateState extends DrawableWrapper.DrawableWrapperState { 330 boolean mPivotXRel = true; 331 float mPivotX = 0.5f; 332 boolean mPivotYRel = true; 333 float mPivotY = 0.5f; 334 float mFromDegrees = 0.0f; 335 float mToDegrees = 360.0f; 336 float mCurrentDegrees = 0.0f; 337 338 RotateState(RotateState orig) { 339 super(orig); 340 341 if (orig != null) { 342 mPivotXRel = orig.mPivotXRel; 343 mPivotX = orig.mPivotX; 344 mPivotYRel = orig.mPivotYRel; 345 mPivotY = orig.mPivotY; 346 mFromDegrees = orig.mFromDegrees; 347 mToDegrees = orig.mToDegrees; 348 mCurrentDegrees = orig.mCurrentDegrees; 349 } 350 } 351 352 @Override 353 public Drawable newDrawable(Resources res) { 354 return new RotateDrawable(this, res); 355 } 356 } 357 358 private RotateDrawable(RotateState state, Resources res) { 359 super(state, res); 360 361 mState = state; 362 } 363} 364