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.graphics.ColorFilter;
24 import android.graphics.PorterDuff.Mode;
25 import android.content.res.ColorStateList;
26 import android.content.res.Resources;
27 import android.content.res.TypedArray;
28 import android.content.res.Resources.Theme;
29 import android.util.AttributeSet;
30 import android.util.TypedValue;
31 import android.util.Log;
32 import android.os.SystemClock;
33 
34 import org.xmlpull.v1.XmlPullParser;
35 import org.xmlpull.v1.XmlPullParserException;
36 
37 import java.io.IOException;
38 
39 import com.android.internal.R;
40 
41 /**
42  * @hide
43  */
44 public class AnimatedRotateDrawable extends Drawable implements Drawable.Callback, Runnable,
45         Animatable {
46     private static final String TAG = "AnimatedRotateDrawable";
47 
48     private AnimatedRotateState mState;
49     private boolean mMutated;
50     private float mCurrentDegrees;
51     private float mIncrement;
52     private boolean mRunning;
53 
AnimatedRotateDrawable()54     public AnimatedRotateDrawable() {
55         this(null, null);
56     }
57 
AnimatedRotateDrawable(AnimatedRotateState rotateState, Resources res)58     private AnimatedRotateDrawable(AnimatedRotateState rotateState, Resources res) {
59         mState = new AnimatedRotateState(rotateState, this, res);
60         init();
61     }
62 
init()63     private void init() {
64         final AnimatedRotateState state = mState;
65         mIncrement = 360.0f / state.mFramesCount;
66         final Drawable drawable = state.mDrawable;
67         if (drawable != null) {
68             drawable.setFilterBitmap(true);
69             if (drawable instanceof BitmapDrawable) {
70                 ((BitmapDrawable) drawable).setAntiAlias(true);
71             }
72         }
73     }
74 
75     @Override
draw(Canvas canvas)76     public void draw(Canvas canvas) {
77         int saveCount = canvas.save();
78 
79         final AnimatedRotateState st = mState;
80         final Drawable drawable = st.mDrawable;
81         final Rect bounds = drawable.getBounds();
82 
83         int w = bounds.right - bounds.left;
84         int h = bounds.bottom - bounds.top;
85 
86         float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX;
87         float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY;
88 
89         canvas.rotate(mCurrentDegrees, px + bounds.left, py + bounds.top);
90 
91         drawable.draw(canvas);
92 
93         canvas.restoreToCount(saveCount);
94     }
95 
96     @Override
start()97     public void start() {
98         if (!mRunning) {
99             mRunning = true;
100             nextFrame();
101         }
102     }
103 
104     @Override
stop()105     public void stop() {
106         mRunning = false;
107         unscheduleSelf(this);
108     }
109 
110     @Override
isRunning()111     public boolean isRunning() {
112         return mRunning;
113     }
114 
nextFrame()115     private void nextFrame() {
116         unscheduleSelf(this);
117         scheduleSelf(this, SystemClock.uptimeMillis() + mState.mFrameDuration);
118     }
119 
120     @Override
run()121     public void run() {
122         // TODO: This should be computed in draw(Canvas), based on the amount
123         // of time since the last frame drawn
124         mCurrentDegrees += mIncrement;
125         if (mCurrentDegrees > (360.0f - mIncrement)) {
126             mCurrentDegrees = 0.0f;
127         }
128         invalidateSelf();
129         nextFrame();
130     }
131 
132     @Override
setVisible(boolean visible, boolean restart)133     public boolean setVisible(boolean visible, boolean restart) {
134         mState.mDrawable.setVisible(visible, restart);
135         boolean changed = super.setVisible(visible, restart);
136         if (visible) {
137             if (changed || restart) {
138                 mCurrentDegrees = 0.0f;
139                 nextFrame();
140             }
141         } else {
142             unscheduleSelf(this);
143         }
144         return changed;
145     }
146 
147     /**
148      * Returns the drawable rotated by this RotateDrawable.
149      */
getDrawable()150     public Drawable getDrawable() {
151         return mState.mDrawable;
152     }
153 
154     @Override
getChangingConfigurations()155     public int getChangingConfigurations() {
156         return super.getChangingConfigurations()
157                 | mState.mChangingConfigurations
158                 | mState.mDrawable.getChangingConfigurations();
159     }
160 
161     @Override
setAlpha(int alpha)162     public void setAlpha(int alpha) {
163         mState.mDrawable.setAlpha(alpha);
164     }
165 
166     @Override
getAlpha()167     public int getAlpha() {
168         return mState.mDrawable.getAlpha();
169     }
170 
171     @Override
setColorFilter(ColorFilter cf)172     public void setColorFilter(ColorFilter cf) {
173         mState.mDrawable.setColorFilter(cf);
174     }
175 
176     @Override
setTintList(ColorStateList tint)177     public void setTintList(ColorStateList tint) {
178         mState.mDrawable.setTintList(tint);
179     }
180 
181     @Override
setTintMode(Mode tintMode)182     public void setTintMode(Mode tintMode) {
183         mState.mDrawable.setTintMode(tintMode);
184     }
185 
186     @Override
getOpacity()187     public int getOpacity() {
188         return mState.mDrawable.getOpacity();
189     }
190 
191     @Override
canApplyTheme()192     public boolean canApplyTheme() {
193         return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
194     }
195 
196     @Override
invalidateDrawable(Drawable who)197     public void invalidateDrawable(Drawable who) {
198         final Callback callback = getCallback();
199         if (callback != null) {
200             callback.invalidateDrawable(this);
201         }
202     }
203 
204     @Override
scheduleDrawable(Drawable who, Runnable what, long when)205     public void scheduleDrawable(Drawable who, Runnable what, long when) {
206         final Callback callback = getCallback();
207         if (callback != null) {
208             callback.scheduleDrawable(this, what, when);
209         }
210     }
211 
212     @Override
unscheduleDrawable(Drawable who, Runnable what)213     public void unscheduleDrawable(Drawable who, Runnable what) {
214         final Callback callback = getCallback();
215         if (callback != null) {
216             callback.unscheduleDrawable(this, what);
217         }
218     }
219 
220     @Override
getPadding(Rect padding)221     public boolean getPadding(Rect padding) {
222         return mState.mDrawable.getPadding(padding);
223     }
224 
225     @Override
isStateful()226     public boolean isStateful() {
227         return mState.mDrawable.isStateful();
228     }
229 
230     @Override
onBoundsChange(Rect bounds)231     protected void onBoundsChange(Rect bounds) {
232         mState.mDrawable.setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
233     }
234 
235     @Override
onLevelChange(int level)236     protected boolean onLevelChange(int level) {
237         return mState.mDrawable.setLevel(level);
238     }
239 
240     @Override
onStateChange(int[] state)241     protected boolean onStateChange(int[] state) {
242         return mState.mDrawable.setState(state);
243     }
244 
245     @Override
getIntrinsicWidth()246     public int getIntrinsicWidth() {
247         return mState.mDrawable.getIntrinsicWidth();
248     }
249 
250     @Override
getIntrinsicHeight()251     public int getIntrinsicHeight() {
252         return mState.mDrawable.getIntrinsicHeight();
253     }
254 
255     @Override
getConstantState()256     public ConstantState getConstantState() {
257         if (mState.canConstantState()) {
258             mState.mChangingConfigurations = getChangingConfigurations();
259             return mState;
260         }
261         return null;
262     }
263 
264     @Override
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)265     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
266             @NonNull AttributeSet attrs, @Nullable Theme theme)
267             throws XmlPullParserException, IOException {
268         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimatedRotateDrawable);
269         super.inflateWithAttributes(r, parser, a, R.styleable.AnimatedRotateDrawable_visible);
270         updateStateFromTypedArray(a);
271         a.recycle();
272 
273         inflateChildElements(r, parser, attrs, theme);
274 
275         init();
276     }
277 
278     @Override
applyTheme(@ullable Theme t)279     public void applyTheme(@Nullable Theme t) {
280         super.applyTheme(t);
281 
282         final AnimatedRotateState state = mState;
283         if (state == null) {
284             return;
285         }
286 
287         if (state.mThemeAttrs != null) {
288             final TypedArray a = t.resolveAttributes(
289                     state.mThemeAttrs, R.styleable.AnimatedRotateDrawable);
290             try {
291                 updateStateFromTypedArray(a);
292                 verifyRequiredAttributes(a);
293             } catch (XmlPullParserException e) {
294                 throw new RuntimeException(e);
295             } finally {
296                 a.recycle();
297             }
298         }
299 
300         if (state.mDrawable != null && state.mDrawable.canApplyTheme()) {
301             state.mDrawable.applyTheme(t);
302         }
303 
304         init();
305     }
306 
updateStateFromTypedArray(TypedArray a)307     private void updateStateFromTypedArray(TypedArray a) {
308         final AnimatedRotateState state = mState;
309 
310         // Account for any configuration changes.
311         state.mChangingConfigurations |= a.getChangingConfigurations();
312 
313         // Extract the theme attributes, if any.
314         state.mThemeAttrs = a.extractThemeAttrs();
315 
316         if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotX)) {
317             final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotX);
318             state.mPivotXRel = tv.type == TypedValue.TYPE_FRACTION;
319             state.mPivotX = state.mPivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
320         }
321 
322         if (a.hasValue(R.styleable.AnimatedRotateDrawable_pivotY)) {
323             final TypedValue tv = a.peekValue(R.styleable.AnimatedRotateDrawable_pivotY);
324             state.mPivotYRel = tv.type == TypedValue.TYPE_FRACTION;
325             state.mPivotY = state.mPivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
326         }
327 
328         setFramesCount(a.getInt(
329                 R.styleable.AnimatedRotateDrawable_framesCount, state.mFramesCount));
330         setFramesDuration(a.getInt(
331                 R.styleable.AnimatedRotateDrawable_frameDuration, state.mFrameDuration));
332 
333         final Drawable dr = a.getDrawable(R.styleable.AnimatedRotateDrawable_drawable);
334         if (dr != null) {
335             state.mDrawable = dr;
336             dr.setCallback(this);
337         }
338     }
339 
inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)340     private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
341             Theme theme) throws XmlPullParserException, IOException {
342         final AnimatedRotateState state = mState;
343 
344         Drawable dr = null;
345         int outerDepth = parser.getDepth();
346         int type;
347         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
348                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
349             if (type != XmlPullParser.START_TAG) {
350                 continue;
351             }
352 
353             if ((dr = Drawable.createFromXmlInner(r, parser, attrs, theme)) == null) {
354                 Log.w(TAG, "Bad element under <animated-rotate>: " + parser.getName());
355             }
356         }
357 
358         if (dr != null) {
359             state.mDrawable = dr;
360             dr.setCallback(this);
361         }
362     }
363 
verifyRequiredAttributes(TypedArray a)364     private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
365         // If we're not waiting on a theme, verify required attributes.
366         if (mState.mDrawable == null && (mState.mThemeAttrs == null
367                 || mState.mThemeAttrs[R.styleable.AnimatedRotateDrawable_drawable] == 0)) {
368             throw new XmlPullParserException(a.getPositionDescription()
369                     + ": <animated-rotate> tag requires a 'drawable' attribute or "
370                     + "child tag defining a drawable");
371         }
372     }
373 
setFramesCount(int framesCount)374     public void setFramesCount(int framesCount) {
375         mState.mFramesCount = framesCount;
376         mIncrement = 360.0f / mState.mFramesCount;
377     }
378 
setFramesDuration(int framesDuration)379     public void setFramesDuration(int framesDuration) {
380         mState.mFrameDuration = framesDuration;
381     }
382 
383     @Override
mutate()384     public Drawable mutate() {
385         if (!mMutated && super.mutate() == this) {
386             mState.mDrawable.mutate();
387             mMutated = true;
388         }
389         return this;
390     }
391 
392     /**
393      * @hide
394      */
clearMutated()395     public void clearMutated() {
396         super.clearMutated();
397         mState.mDrawable.clearMutated();
398         mMutated = false;
399     }
400 
401     final static class AnimatedRotateState extends Drawable.ConstantState {
402         Drawable mDrawable;
403         int[] mThemeAttrs;
404 
405         int mChangingConfigurations;
406 
407         boolean mPivotXRel = false;
408         float mPivotX = 0;
409         boolean mPivotYRel = false;
410         float mPivotY = 0;
411         int mFrameDuration = 150;
412         int mFramesCount = 12;
413 
414         private boolean mCanConstantState;
415         private boolean mCheckedConstantState;
416 
AnimatedRotateState(AnimatedRotateState orig, AnimatedRotateDrawable owner, Resources res)417         public AnimatedRotateState(AnimatedRotateState orig, AnimatedRotateDrawable owner,
418                 Resources res) {
419             if (orig != null) {
420                 if (res != null) {
421                     mDrawable = orig.mDrawable.getConstantState().newDrawable(res);
422                 } else {
423                     mDrawable = orig.mDrawable.getConstantState().newDrawable();
424                 }
425                 mDrawable.setCallback(owner);
426                 mDrawable.setLayoutDirection(orig.mDrawable.getLayoutDirection());
427                 mDrawable.setBounds(orig.mDrawable.getBounds());
428                 mDrawable.setLevel(orig.mDrawable.getLevel());
429                 mThemeAttrs = orig.mThemeAttrs;
430                 mPivotXRel = orig.mPivotXRel;
431                 mPivotX = orig.mPivotX;
432                 mPivotYRel = orig.mPivotYRel;
433                 mPivotY = orig.mPivotY;
434                 mFramesCount = orig.mFramesCount;
435                 mFrameDuration = orig.mFrameDuration;
436                 mCanConstantState = mCheckedConstantState = true;
437             }
438         }
439 
440         @Override
newDrawable()441         public Drawable newDrawable() {
442             return new AnimatedRotateDrawable(this, null);
443         }
444 
445         @Override
newDrawable(Resources res)446         public Drawable newDrawable(Resources res) {
447             return new AnimatedRotateDrawable(this, res);
448         }
449 
450         @Override
canApplyTheme()451         public boolean canApplyTheme() {
452             return mThemeAttrs != null || (mDrawable != null && mDrawable.canApplyTheme())
453                     || super.canApplyTheme();
454         }
455 
456         @Override
getChangingConfigurations()457         public int getChangingConfigurations() {
458             return mChangingConfigurations;
459         }
460 
canConstantState()461         public boolean canConstantState() {
462             if (!mCheckedConstantState) {
463                 mCanConstantState = mDrawable.getConstantState() != null;
464                 mCheckedConstantState = true;
465             }
466 
467             return mCanConstantState;
468         }
469     }
470 }
471