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 17 package android.graphics.drawable; 18 19 import com.android.internal.R; 20 21 import org.xmlpull.v1.XmlPullParser; 22 import org.xmlpull.v1.XmlPullParserException; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.graphics.Canvas; 27 import android.graphics.Rect; 28 import android.content.res.Resources; 29 import android.content.res.TypedArray; 30 import android.content.res.Resources.Theme; 31 import android.util.MathUtils; 32 import android.util.TypedValue; 33 import android.util.AttributeSet; 34 35 import 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 */ 54 public 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 */ RotateDrawable()62 public RotateDrawable() { 63 this(new RotateState(null, null), null); 64 } 65 66 @Override inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)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 applyTheme(@onNull Theme t)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 rethrowAsRuntimeException(e); 96 } finally { 97 a.recycle(); 98 } 99 } 100 } 101 verifyRequiredAttributes(@onNull TypedArray a)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 updateStateFromTypedArray(@onNull TypedArray a)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 draw(Canvas canvas)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 */ setFromDegrees(float fromDegrees)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 */ getFromDegrees()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 */ setToDegrees(float toDegrees)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 */ getToDegrees()201 public float getToDegrees() { 202 return mState.mToDegrees; 203 } 204 205 /** 206 * Sets the X position around which the drawable is rotated. 207 * <p> 208 * If the X pivot is relative (as specified by 209 * {@link #setPivotXRelative(boolean)}), then the position represents a 210 * fraction of the drawable width. Otherwise, the position represents an 211 * absolute value in pixels. 212 * 213 * @param pivotX X position around which to rotate 214 * @see #setPivotXRelative(boolean) 215 * @attr ref android.R.styleable#RotateDrawable_pivotX 216 */ setPivotX(float pivotX)217 public void setPivotX(float pivotX) { 218 if (mState.mPivotX != pivotX) { 219 mState.mPivotX = pivotX; 220 invalidateSelf(); 221 } 222 } 223 224 /** 225 * @return X position around which to rotate 226 * @see #setPivotX(float) 227 * @attr ref android.R.styleable#RotateDrawable_pivotX 228 */ getPivotX()229 public float getPivotX() { 230 return mState.mPivotX; 231 } 232 233 /** 234 * Sets whether the X pivot value represents a fraction of the drawable 235 * width or an absolute value in pixels. 236 * 237 * @param relative true if the X pivot represents a fraction of the drawable 238 * width, or false if it represents an absolute value in pixels 239 * @see #isPivotXRelative() 240 */ setPivotXRelative(boolean relative)241 public void setPivotXRelative(boolean relative) { 242 if (mState.mPivotXRel != relative) { 243 mState.mPivotXRel = relative; 244 invalidateSelf(); 245 } 246 } 247 248 /** 249 * @return true if the X pivot represents a fraction of the drawable width, 250 * or false if it represents an absolute value in pixels 251 * @see #setPivotXRelative(boolean) 252 */ isPivotXRelative()253 public boolean isPivotXRelative() { 254 return mState.mPivotXRel; 255 } 256 257 /** 258 * Sets the Y position around which the drawable is rotated. 259 * <p> 260 * If the Y pivot is relative (as specified by 261 * {@link #setPivotYRelative(boolean)}), then the position represents a 262 * fraction of the drawable height. Otherwise, the position represents an 263 * absolute value in pixels. 264 * 265 * @param pivotY Y position around which to rotate 266 * @see #getPivotY() 267 * @attr ref android.R.styleable#RotateDrawable_pivotY 268 */ setPivotY(float pivotY)269 public void setPivotY(float pivotY) { 270 if (mState.mPivotY != pivotY) { 271 mState.mPivotY = pivotY; 272 invalidateSelf(); 273 } 274 } 275 276 /** 277 * @return Y position around which to rotate 278 * @see #setPivotY(float) 279 * @attr ref android.R.styleable#RotateDrawable_pivotY 280 */ getPivotY()281 public float getPivotY() { 282 return mState.mPivotY; 283 } 284 285 /** 286 * Sets whether the Y pivot value represents a fraction of the drawable 287 * height or an absolute value in pixels. 288 * 289 * @param relative True if the Y pivot represents a fraction of the drawable 290 * height, or false if it represents an absolute value in pixels 291 * @see #isPivotYRelative() 292 */ setPivotYRelative(boolean relative)293 public void setPivotYRelative(boolean relative) { 294 if (mState.mPivotYRel != relative) { 295 mState.mPivotYRel = relative; 296 invalidateSelf(); 297 } 298 } 299 300 /** 301 * @return true if the Y pivot represents a fraction of the drawable height, 302 * or false if it represents an absolute value in pixels 303 * @see #setPivotYRelative(boolean) 304 */ isPivotYRelative()305 public boolean isPivotYRelative() { 306 return mState.mPivotYRel; 307 } 308 309 @Override onLevelChange(int level)310 protected boolean onLevelChange(int level) { 311 super.onLevelChange(level); 312 313 final float value = level / (float) MAX_LEVEL; 314 final float degrees = MathUtils.lerp(mState.mFromDegrees, mState.mToDegrees, value); 315 mState.mCurrentDegrees = degrees; 316 317 invalidateSelf(); 318 return true; 319 } 320 321 @Override mutateConstantState()322 DrawableWrapperState mutateConstantState() { 323 mState = new RotateState(mState, null); 324 return mState; 325 } 326 327 static final class RotateState extends DrawableWrapper.DrawableWrapperState { 328 private int[] mThemeAttrs; 329 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 RotateState(RotateState orig, Resources res)338 RotateState(RotateState orig, Resources res) { 339 super(orig, res); 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 newDrawable(Resources res)353 public Drawable newDrawable(Resources res) { 354 return new RotateDrawable(this, res); 355 } 356 } 357 RotateDrawable(RotateState state, Resources res)358 private RotateDrawable(RotateState state, Resources res) { 359 super(state, res); 360 361 mState = state; 362 } 363 } 364