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