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>&lt;rotate&gt;</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