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);
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         super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedRotateDrawable_visible);
130 
131         updateStateFromTypedArray(a);
132         inflateChildDrawable(r, parser, attrs, theme);
133         verifyRequiredAttributes(a);
134         a.recycle();
135 
136         updateLocalState();
137     }
138 
verifyRequiredAttributes(TypedArray a)139     private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
140         // If we're not waiting on a theme, verify required attributes.
141         if (getDrawable() == null && (mState.mThemeAttrs == null
142                 || mState.mThemeAttrs[R.styleable.AnimatedRotateDrawable_drawable] == 0)) {
143             throw new XmlPullParserException(a.getPositionDescription()
144                     + ": <animated-rotate> tag requires a 'drawable' attribute or "
145                     + "child tag defining a drawable");
146         }
147     }
148 
149     @Override
updateStateFromTypedArray(TypedArray a)150     void updateStateFromTypedArray(TypedArray a) {
151         super.updateStateFromTypedArray(a);
152 
153         final AnimatedRotateState state = mState;
154 
155         if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotX)) {
156             final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotX);
157             state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION;
158             state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
159         }
160 
161         if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotY)) {
162             final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotY);
163             state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION;
164             state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
165         }
166 
167         setFramesCount(a.getInt(
168                 R.styleable.AnimatedRotateDrawable_framesCount, state.mFramesCount));
169         setFramesDuration(a.getInt(
170                 R.styleable.AnimatedRotateDrawable_frameDuration, state.mFrameDuration));
171 
172         final Drawable dr = a.getDrawable(R.styleable.AnimatedRotateDrawable_drawable);
173         if (dr != null) {
174             setDrawable(dr);
175         }
176     }
177 
178     @Override
applyTheme(@ullable Theme t)179     public void applyTheme(@Nullable Theme t) {
180         final AnimatedRotateState state = mState;
181         if (state == null) {
182             return;
183         }
184 
185         if (state.mThemeAttrs != null) {
186             final TypedArray a = t.resolveAttributes(
187                     state.mThemeAttrs, R.styleable.AnimatedRotateDrawable);
188             try {
189                 updateStateFromTypedArray(a);
190                 verifyRequiredAttributes(a);
191             } catch (XmlPullParserException e) {
192                 throw new RuntimeException(e);
193             } finally {
194                 a.recycle();
195             }
196         }
197 
198         // The drawable may have changed as a result of applying the theme, so
199         // apply the theme to the wrapped drawable last.
200         super.applyTheme(t);
201 
202         updateLocalState();
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     static final class AnimatedRotateState extends DrawableWrapper.DrawableWrapperState {
215         boolean mPivotXRel = false;
216         float mPivotX = 0;
217         boolean mPivotYRel = false;
218         float mPivotY = 0;
219         int mFrameDuration = 150;
220         int mFramesCount = 12;
221 
AnimatedRotateState(AnimatedRotateState orig)222         public AnimatedRotateState(AnimatedRotateState orig) {
223             super(orig);
224 
225             if (orig != null) {
226                 mPivotXRel = orig.mPivotXRel;
227                 mPivotX = orig.mPivotX;
228                 mPivotYRel = orig.mPivotYRel;
229                 mPivotY = orig.mPivotY;
230                 mFramesCount = orig.mFramesCount;
231                 mFrameDuration = orig.mFrameDuration;
232             }
233         }
234 
235         @Override
newDrawable(Resources res)236         public Drawable newDrawable(Resources res) {
237             return new AnimatedRotateDrawable(this, res);
238         }
239     }
240 
AnimatedRotateDrawable(AnimatedRotateState state, Resources res)241     private AnimatedRotateDrawable(AnimatedRotateState state, Resources res) {
242         super(state, res);
243 
244         mState = state;
245 
246         updateLocalState();
247     }
248 
updateLocalState()249     private void updateLocalState() {
250         final AnimatedRotateState state = mState;
251         mIncrement = 360.0f / state.mFramesCount;
252 
253         // Force the wrapped drawable to use filtering and AA, if applicable,
254         // so that it looks smooth when rotated.
255         final Drawable drawable = getDrawable();
256         if (drawable != null) {
257             drawable.setFilterBitmap(true);
258             if (drawable instanceof BitmapDrawable) {
259                 ((BitmapDrawable) drawable).setAntiAlias(true);
260             }
261         }
262     }
263 
264     private final Runnable mNextFrame = new Runnable() {
265         @Override
266         public void run() {
267             // TODO: This should be computed in draw(Canvas), based on the amount
268             // of time since the last frame drawn
269             mCurrentDegrees += mIncrement;
270             if (mCurrentDegrees > (360.0f - mIncrement)) {
271                 mCurrentDegrees = 0.0f;
272             }
273             invalidateSelf();
274             nextFrame();
275         }
276     };
277 }
278