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