1 /*
2  * Copyright (C) 2009 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.graphics.Canvas;
22 import android.graphics.Rect;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.content.res.Resources.Theme;
26 import android.util.AttributeSet;
27 import android.util.TypedValue;
28 import android.os.SystemClock;
29 
30 import org.xmlpull.v1.XmlPullParser;
31 import org.xmlpull.v1.XmlPullParserException;
32 
33 import java.io.IOException;
34 
35 import com.android.internal.R;
36 
37 /**
38  * @hide
39  */
40 public class AnimatedRotateDrawable extends DrawableWrapper implements Animatable {
41     private AnimatedRotateState mState;
42 
43     private float mCurrentDegrees;
44     private float mIncrement;
45 
46     /** Whether this drawable is currently animating. */
47     private boolean mRunning;
48 
49     /**
50      * Creates a new animated rotating drawable with no wrapped drawable.
51      */
AnimatedRotateDrawable()52     public AnimatedRotateDrawable() {
53         this(new AnimatedRotateState(null, null), null);
54     }
55 
56     @Override
draw(Canvas canvas)57     public void draw(Canvas canvas) {
58         final Drawable drawable = getDrawable();
59         final Rect bounds = drawable.getBounds();
60         final int w = bounds.right - bounds.left;
61         final int h = bounds.bottom - bounds.top;
62 
63         final AnimatedRotateState st = mState;
64         final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX;
65         final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY;
66 
67         final int saveCount = canvas.save();
68         canvas.rotate(mCurrentDegrees, px + bounds.left, py + bounds.top);
69         drawable.draw(canvas);
70         canvas.restoreToCount(saveCount);
71     }
72 
73     /**
74      * Starts the rotation animation.
75      * <p>
76      * The animation will run until {@link #stop()} is called. Calling this
77      * method while the animation is already running has no effect.
78      *
79      * @see #stop()
80      */
81     @Override
start()82     public void start() {
83         if (!mRunning) {
84             mRunning = true;
85             nextFrame();
86         }
87     }
88 
89     /**
90      * Stops the rotation animation.
91      *
92      * @see #start()
93      */
94     @Override
stop()95     public void stop() {
96         mRunning = false;
97         unscheduleSelf(mNextFrame);
98     }
99 
100     @Override
isRunning()101     public boolean isRunning() {
102         return mRunning;
103     }
104 
nextFrame()105     private void nextFrame() {
106         unscheduleSelf(mNextFrame);
107         scheduleSelf(mNextFrame, SystemClock.uptimeMillis() + mState.mFrameDuration);
108     }
109 
110     @Override
setVisible(boolean visible, boolean restart)111     public boolean setVisible(boolean visible, boolean restart) {
112         final boolean changed = super.setVisible(visible, restart);
113         if (visible) {
114             if (changed || restart) {
115                 mCurrentDegrees = 0.0f;
116                 nextFrame();
117             }
118         } else {
119             unscheduleSelf(mNextFrame);
120         }
121         return changed;
122     }
123 
124     @Override
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)125     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
126             @NonNull AttributeSet attrs, @Nullable Theme theme)
127             throws XmlPullParserException, IOException {
128         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedRotateDrawable);
129 
130         // Inflation will advance the XmlPullParser and AttributeSet.
131         super.inflate(r, parser, attrs, theme);
132 
133         updateStateFromTypedArray(a);
134         verifyRequiredAttributes(a);
135         a.recycle();
136 
137         updateLocalState();
138     }
139 
140     @Override
applyTheme(@onNull Theme t)141     public void applyTheme(@NonNull Theme t) {
142         super.applyTheme(t);
143 
144         final AnimatedRotateState state = mState;
145         if (state == null) {
146             return;
147         }
148 
149         if (state.mThemeAttrs != null) {
150             final TypedArray a = t.resolveAttributes(
151                     state.mThemeAttrs, R.styleable.AnimatedRotateDrawable);
152             try {
153                 updateStateFromTypedArray(a);
154                 verifyRequiredAttributes(a);
155             } catch (XmlPullParserException e) {
156                 rethrowAsRuntimeException(e);
157             } finally {
158                 a.recycle();
159             }
160         }
161 
162         updateLocalState();
163     }
164 
verifyRequiredAttributes(@onNull TypedArray a)165     private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
166         // If we're not waiting on a theme, verify required attributes.
167         if (getDrawable() == null && (mState.mThemeAttrs == null
168                 || mState.mThemeAttrs[R.styleable.AnimatedRotateDrawable_drawable] == 0)) {
169             throw new XmlPullParserException(a.getPositionDescription()
170                     + ": <animated-rotate> tag requires a 'drawable' attribute or "
171                     + "child tag defining a drawable");
172         }
173     }
174 
updateStateFromTypedArray(@onNull TypedArray a)175     private void updateStateFromTypedArray(@NonNull TypedArray a) {
176         final AnimatedRotateState state = mState;
177         if (state == null) {
178             return;
179         }
180 
181         // Account for any configuration changes.
182         state.mChangingConfigurations |= a.getChangingConfigurations();
183 
184         // Extract the theme attributes, if any.
185         state.mThemeAttrs = a.extractThemeAttrs();
186 
187         if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotX)) {
188             final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotX);
189             state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION;
190             state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
191         }
192 
193         if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotY)) {
194             final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotY);
195             state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION;
196             state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
197         }
198 
199         setFramesCount(a.getInt(
200                 R.styleable.AnimatedRotateDrawable_framesCount, state.mFramesCount));
201         setFramesDuration(a.getInt(
202                 R.styleable.AnimatedRotateDrawable_frameDuration, state.mFrameDuration));
203     }
204 
setFramesCount(int framesCount)205     public void setFramesCount(int framesCount) {
206         mState.mFramesCount = framesCount;
207         mIncrement = 360.0f / mState.mFramesCount;
208     }
209 
setFramesDuration(int framesDuration)210     public void setFramesDuration(int framesDuration) {
211         mState.mFrameDuration = framesDuration;
212     }
213 
214     @Override
mutateConstantState()215     DrawableWrapperState mutateConstantState() {
216         mState = new AnimatedRotateState(mState, null);
217         return mState;
218     }
219 
220     static final class AnimatedRotateState extends DrawableWrapper.DrawableWrapperState {
221         private int[] mThemeAttrs;
222 
223         boolean mPivotXRel = false;
224         float mPivotX = 0;
225         boolean mPivotYRel = false;
226         float mPivotY = 0;
227         int mFrameDuration = 150;
228         int mFramesCount = 12;
229 
AnimatedRotateState(AnimatedRotateState orig, Resources res)230         public AnimatedRotateState(AnimatedRotateState orig, Resources res) {
231             super(orig, res);
232 
233             if (orig != null) {
234                 mPivotXRel = orig.mPivotXRel;
235                 mPivotX = orig.mPivotX;
236                 mPivotYRel = orig.mPivotYRel;
237                 mPivotY = orig.mPivotY;
238                 mFramesCount = orig.mFramesCount;
239                 mFrameDuration = orig.mFrameDuration;
240             }
241         }
242 
243         @Override
newDrawable(Resources res)244         public Drawable newDrawable(Resources res) {
245             return new AnimatedRotateDrawable(this, res);
246         }
247     }
248 
AnimatedRotateDrawable(AnimatedRotateState state, Resources res)249     private AnimatedRotateDrawable(AnimatedRotateState state, Resources res) {
250         super(state, res);
251 
252         mState = state;
253 
254         updateLocalState();
255     }
256 
updateLocalState()257     private void updateLocalState() {
258         final AnimatedRotateState state = mState;
259         mIncrement = 360.0f / state.mFramesCount;
260 
261         // Force the wrapped drawable to use filtering and AA, if applicable,
262         // so that it looks smooth when rotated.
263         final Drawable drawable = getDrawable();
264         if (drawable != null) {
265             drawable.setFilterBitmap(true);
266             if (drawable instanceof BitmapDrawable) {
267                 ((BitmapDrawable) drawable).setAntiAlias(true);
268             }
269         }
270     }
271 
272     private final Runnable mNextFrame = new Runnable() {
273         @Override
274         public void run() {
275             // TODO: This should be computed in draw(Canvas), based on the amount
276             // of time since the last frame drawn
277             mCurrentDegrees += mIncrement;
278             if (mCurrentDegrees > (360.0f - mIncrement)) {
279                 mCurrentDegrees = 0.0f;
280             }
281             invalidateSelf();
282             nextFrame();
283         }
284     };
285 }
286