RotateDrawable.java revision 7e3ede288926bdfb79b1571fed74cad614935821
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.annotation.NonNull; 25import android.annotation.Nullable; 26import android.graphics.Canvas; 27import android.graphics.Rect; 28import android.content.res.Resources; 29import android.content.res.TypedArray; 30import android.content.res.Resources.Theme; 31import android.util.MathUtils; 32import android.util.TypedValue; 33import android.util.AttributeSet; 34 35import java.io.IOException; 36 37/** 38 * <p> 39 * A Drawable that can rotate another Drawable based on the current level value. 40 * The start and end angles of rotation can be controlled to map any circular 41 * arc to the level values range. 42 * <p> 43 * It can be defined in an XML file with the <code><rotate></code> element. 44 * For more information, see the guide to 45 * <a href="{@docRoot}guide/topics/resources/animation-resource.html">Animation Resources</a>. 46 * 47 * @attr ref android.R.styleable#RotateDrawable_visible 48 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 49 * @attr ref android.R.styleable#RotateDrawable_toDegrees 50 * @attr ref android.R.styleable#RotateDrawable_pivotX 51 * @attr ref android.R.styleable#RotateDrawable_pivotY 52 * @attr ref android.R.styleable#RotateDrawable_drawable 53 */ 54public class RotateDrawable extends DrawableWrapper { 55 private static final int MAX_LEVEL = 10000; 56 57 private RotateState mState; 58 59 /** 60 * Creates a new rotating drawable with no wrapped drawable. 61 */ 62 public RotateDrawable() { 63 this(new RotateState(null, null), null); 64 } 65 66 @Override 67 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 68 @NonNull AttributeSet attrs, @Nullable Theme theme) 69 throws XmlPullParserException, IOException { 70 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RotateDrawable); 71 72 // Inflation will advance the XmlPullParser and AttributeSet. 73 super.inflate(r, parser, attrs, theme); 74 75 updateStateFromTypedArray(a); 76 verifyRequiredAttributes(a); 77 a.recycle(); 78 } 79 80 @Override 81 public void applyTheme(@NonNull Theme t) { 82 super.applyTheme(t); 83 84 final RotateState state = mState; 85 if (state == null) { 86 return; 87 } 88 89 if (state.mThemeAttrs != null) { 90 final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.RotateDrawable); 91 try { 92 updateStateFromTypedArray(a); 93 verifyRequiredAttributes(a); 94 } catch (XmlPullParserException e) { 95 throw new RuntimeException(e); 96 } finally { 97 a.recycle(); 98 } 99 } 100 } 101 102 private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException { 103 // If we're not waiting on a theme, verify required attributes. 104 if (getDrawable() == null && (mState.mThemeAttrs == null 105 || mState.mThemeAttrs[R.styleable.RotateDrawable_drawable] == 0)) { 106 throw new XmlPullParserException(a.getPositionDescription() 107 + ": <rotate> tag requires a 'drawable' attribute or " 108 + "child tag defining a drawable"); 109 } 110 } 111 112 private void updateStateFromTypedArray(@NonNull TypedArray a) { 113 final RotateState state = mState; 114 if (state == null) { 115 return; 116 } 117 118 // Account for any configuration changes. 119 state.mChangingConfigurations |= a.getChangingConfigurations(); 120 121 // Extract the theme attributes, if any. 122 state.mThemeAttrs = a.extractThemeAttrs(); 123 124 if (a.hasValue(R.styleable.RotateDrawable_pivotX)) { 125 final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotX); 126 state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION; 127 state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 128 } 129 130 if (a.hasValue(R.styleable.RotateDrawable_pivotY)) { 131 final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotY); 132 state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION; 133 state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 134 } 135 136 state.mFromDegrees = a.getFloat( 137 R.styleable.RotateDrawable_fromDegrees, state.mFromDegrees); 138 state.mToDegrees = a.getFloat( 139 R.styleable.RotateDrawable_toDegrees, state.mToDegrees); 140 state.mCurrentDegrees = state.mFromDegrees; 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, null); 320 return mState; 321 } 322 323 static final class RotateState extends DrawableWrapper.DrawableWrapperState { 324 private int[] mThemeAttrs; 325 326 boolean mPivotXRel = true; 327 float mPivotX = 0.5f; 328 boolean mPivotYRel = true; 329 float mPivotY = 0.5f; 330 float mFromDegrees = 0.0f; 331 float mToDegrees = 360.0f; 332 float mCurrentDegrees = 0.0f; 333 334 RotateState(RotateState orig, Resources res) { 335 super(orig, res); 336 337 if (orig != null) { 338 mPivotXRel = orig.mPivotXRel; 339 mPivotX = orig.mPivotX; 340 mPivotYRel = orig.mPivotYRel; 341 mPivotY = orig.mPivotY; 342 mFromDegrees = orig.mFromDegrees; 343 mToDegrees = orig.mToDegrees; 344 mCurrentDegrees = orig.mCurrentDegrees; 345 } 346 } 347 348 @Override 349 public Drawable newDrawable(Resources res) { 350 return new RotateDrawable(this, res); 351 } 352 } 353 354 private RotateDrawable(RotateState state, Resources res) { 355 super(state, res); 356 357 mState = state; 358 } 359} 360