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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.content.res.Resources; 23 import android.content.res.Resources.Theme; 24 import android.content.res.TypedArray; 25 import android.graphics.Canvas; 26 import android.graphics.Rect; 27 import android.util.AttributeSet; 28 import android.util.MathUtils; 29 import android.util.TypedValue; 30 31 import com.android.internal.R; 32 33 import org.xmlpull.v1.XmlPullParser; 34 import org.xmlpull.v1.XmlPullParserException; 35 36 import 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 */ 55 public class RotateDrawable extends DrawableWrapper { 56 private static final int MAX_LEVEL = 10000; 57 58 @UnsupportedAppUsage 59 private RotateState mState; 60 61 /** 62 * Creates a new rotating drawable with no wrapped drawable. 63 */ RotateDrawable()64 public RotateDrawable() { 65 this(new RotateState(null, null), null); 66 } 67 68 @Override inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)69 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 70 @NonNull AttributeSet attrs, @Nullable Theme theme) 71 throws XmlPullParserException, IOException { 72 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RotateDrawable); 73 74 // Inflation will advance the XmlPullParser and AttributeSet. 75 super.inflate(r, parser, attrs, theme); 76 77 updateStateFromTypedArray(a); 78 verifyRequiredAttributes(a); 79 a.recycle(); 80 } 81 82 @Override applyTheme(@onNull Theme t)83 public void applyTheme(@NonNull Theme t) { 84 super.applyTheme(t); 85 86 final RotateState state = mState; 87 if (state == null) { 88 return; 89 } 90 91 if (state.mThemeAttrs != null) { 92 final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.RotateDrawable); 93 try { 94 updateStateFromTypedArray(a); 95 verifyRequiredAttributes(a); 96 } catch (XmlPullParserException e) { 97 rethrowAsRuntimeException(e); 98 } finally { 99 a.recycle(); 100 } 101 } 102 } 103 verifyRequiredAttributes(@onNull TypedArray a)104 private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException { 105 // If we're not waiting on a theme, verify required attributes. 106 if (getDrawable() == null && (mState.mThemeAttrs == null 107 || mState.mThemeAttrs[R.styleable.RotateDrawable_drawable] == 0)) { 108 throw new XmlPullParserException(a.getPositionDescription() 109 + ": <rotate> tag requires a 'drawable' attribute or " 110 + "child tag defining a drawable"); 111 } 112 } 113 updateStateFromTypedArray(@onNull TypedArray a)114 private void updateStateFromTypedArray(@NonNull TypedArray a) { 115 final RotateState state = mState; 116 if (state == null) { 117 return; 118 } 119 120 // Account for any configuration changes. 121 state.mChangingConfigurations |= a.getChangingConfigurations(); 122 123 // Extract the theme attributes, if any. 124 state.mThemeAttrs = a.extractThemeAttrs(); 125 126 if (a.hasValue(R.styleable.RotateDrawable_pivotX)) { 127 final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotX); 128 state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION; 129 state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 130 } 131 132 if (a.hasValue(R.styleable.RotateDrawable_pivotY)) { 133 final TypedValue tv = a.peekValue(R.styleable.RotateDrawable_pivotY); 134 state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION; 135 state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat(); 136 } 137 138 state.mFromDegrees = a.getFloat( 139 R.styleable.RotateDrawable_fromDegrees, state.mFromDegrees); 140 state.mToDegrees = a.getFloat( 141 R.styleable.RotateDrawable_toDegrees, state.mToDegrees); 142 state.mCurrentDegrees = state.mFromDegrees; 143 } 144 145 @Override draw(Canvas canvas)146 public void draw(Canvas canvas) { 147 final Drawable d = getDrawable(); 148 final Rect bounds = d.getBounds(); 149 final int w = bounds.right - bounds.left; 150 final int h = bounds.bottom - bounds.top; 151 final RotateState st = mState; 152 final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX; 153 final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY; 154 155 final int saveCount = canvas.save(); 156 canvas.rotate(st.mCurrentDegrees, px + bounds.left, py + bounds.top); 157 d.draw(canvas); 158 canvas.restoreToCount(saveCount); 159 } 160 161 /** 162 * Sets the start angle for rotation. 163 * 164 * @param fromDegrees starting angle in degrees 165 * @see #getFromDegrees() 166 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 167 */ setFromDegrees(float fromDegrees)168 public void setFromDegrees(float fromDegrees) { 169 if (mState.mFromDegrees != fromDegrees) { 170 mState.mFromDegrees = fromDegrees; 171 invalidateSelf(); 172 } 173 } 174 175 /** 176 * @return starting angle for rotation in degrees 177 * @see #setFromDegrees(float) 178 * @attr ref android.R.styleable#RotateDrawable_fromDegrees 179 */ getFromDegrees()180 public float getFromDegrees() { 181 return mState.mFromDegrees; 182 } 183 184 /** 185 * Sets the end angle for rotation. 186 * 187 * @param toDegrees ending angle in degrees 188 * @see #getToDegrees() 189 * @attr ref android.R.styleable#RotateDrawable_toDegrees 190 */ setToDegrees(float toDegrees)191 public void setToDegrees(float toDegrees) { 192 if (mState.mToDegrees != toDegrees) { 193 mState.mToDegrees = toDegrees; 194 invalidateSelf(); 195 } 196 } 197 198 /** 199 * @return ending angle for rotation in degrees 200 * @see #setToDegrees(float) 201 * @attr ref android.R.styleable#RotateDrawable_toDegrees 202 */ getToDegrees()203 public float getToDegrees() { 204 return mState.mToDegrees; 205 } 206 207 /** 208 * Sets the X position around which the drawable is rotated. 209 * <p> 210 * If the X pivot is relative (as specified by 211 * {@link #setPivotXRelative(boolean)}), then the position represents a 212 * fraction of the drawable width. Otherwise, the position represents an 213 * absolute value in pixels. 214 * 215 * @param pivotX X position around which to rotate 216 * @see #setPivotXRelative(boolean) 217 * @attr ref android.R.styleable#RotateDrawable_pivotX 218 */ setPivotX(float pivotX)219 public void setPivotX(float pivotX) { 220 if (mState.mPivotX != pivotX) { 221 mState.mPivotX = pivotX; 222 invalidateSelf(); 223 } 224 } 225 226 /** 227 * @return X position around which to rotate 228 * @see #setPivotX(float) 229 * @attr ref android.R.styleable#RotateDrawable_pivotX 230 */ getPivotX()231 public float getPivotX() { 232 return mState.mPivotX; 233 } 234 235 /** 236 * Sets whether the X pivot value represents a fraction of the drawable 237 * width or an absolute value in pixels. 238 * 239 * @param relative true if the X pivot represents a fraction of the drawable 240 * width, or false if it represents an absolute value in pixels 241 * @see #isPivotXRelative() 242 */ setPivotXRelative(boolean relative)243 public void setPivotXRelative(boolean relative) { 244 if (mState.mPivotXRel != relative) { 245 mState.mPivotXRel = relative; 246 invalidateSelf(); 247 } 248 } 249 250 /** 251 * @return true if the X pivot represents a fraction of the drawable width, 252 * or false if it represents an absolute value in pixels 253 * @see #setPivotXRelative(boolean) 254 */ isPivotXRelative()255 public boolean isPivotXRelative() { 256 return mState.mPivotXRel; 257 } 258 259 /** 260 * Sets the Y position around which the drawable is rotated. 261 * <p> 262 * If the Y pivot is relative (as specified by 263 * {@link #setPivotYRelative(boolean)}), then the position represents a 264 * fraction of the drawable height. Otherwise, the position represents an 265 * absolute value in pixels. 266 * 267 * @param pivotY Y position around which to rotate 268 * @see #getPivotY() 269 * @attr ref android.R.styleable#RotateDrawable_pivotY 270 */ setPivotY(float pivotY)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 */ getPivotY()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 */ setPivotYRelative(boolean relative)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 */ isPivotYRelative()307 public boolean isPivotYRelative() { 308 return mState.mPivotYRel; 309 } 310 311 @Override onLevelChange(int level)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 mutateConstantState()324 DrawableWrapperState mutateConstantState() { 325 mState = new RotateState(mState, null); 326 return mState; 327 } 328 329 static final class RotateState extends DrawableWrapper.DrawableWrapperState { 330 private int[] mThemeAttrs; 331 332 boolean mPivotXRel = true; 333 float mPivotX = 0.5f; 334 boolean mPivotYRel = true; 335 float mPivotY = 0.5f; 336 float mFromDegrees = 0.0f; 337 float mToDegrees = 360.0f; 338 float mCurrentDegrees = 0.0f; 339 RotateState(RotateState orig, Resources res)340 RotateState(RotateState orig, Resources res) { 341 super(orig, res); 342 343 if (orig != null) { 344 mPivotXRel = orig.mPivotXRel; 345 mPivotX = orig.mPivotX; 346 mPivotYRel = orig.mPivotYRel; 347 mPivotY = orig.mPivotY; 348 mFromDegrees = orig.mFromDegrees; 349 mToDegrees = orig.mToDegrees; 350 mCurrentDegrees = orig.mCurrentDegrees; 351 } 352 } 353 354 @Override newDrawable(Resources res)355 public Drawable newDrawable(Resources res) { 356 return new RotateDrawable(this, res); 357 } 358 } 359 RotateDrawable(RotateState state, Resources res)360 private RotateDrawable(RotateState state, Resources res) { 361 super(state, res); 362 363 mState = state; 364 } 365 } 366