AnimatedRotateDrawable.java revision 7e3ede288926bdfb79b1571fed74cad614935821
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
17package android.graphics.drawable;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.graphics.Canvas;
22import android.graphics.Rect;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.content.res.Resources.Theme;
26import android.util.AttributeSet;
27import android.util.TypedValue;
28import android.os.SystemClock;
29
30import org.xmlpull.v1.XmlPullParser;
31import org.xmlpull.v1.XmlPullParserException;
32
33import java.io.IOException;
34
35import com.android.internal.R;
36
37/**
38 * @hide
39 */
40public 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     */
52    public AnimatedRotateDrawable() {
53        this(new AnimatedRotateState(null, null), null);
54    }
55
56    @Override
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
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
95    public void stop() {
96        mRunning = false;
97        unscheduleSelf(mNextFrame);
98    }
99
100    @Override
101    public boolean isRunning() {
102        return mRunning;
103    }
104
105    private void nextFrame() {
106        unscheduleSelf(mNextFrame);
107        scheduleSelf(mNextFrame, SystemClock.uptimeMillis() + mState.mFrameDuration);
108    }
109
110    @Override
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
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
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                throw new RuntimeException(e);
157            } finally {
158                a.recycle();
159            }
160        }
161
162        updateLocalState();
163    }
164
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
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
205    public void setFramesCount(int framesCount) {
206        mState.mFramesCount = framesCount;
207        mIncrement = 360.0f / mState.mFramesCount;
208    }
209
210    public void setFramesDuration(int framesDuration) {
211        mState.mFrameDuration = framesDuration;
212    }
213
214    @Override
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
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
244        public Drawable newDrawable(Resources res) {
245            return new AnimatedRotateDrawable(this, res);
246        }
247    }
248
249    private AnimatedRotateDrawable(AnimatedRotateState state, Resources res) {
250        super(state, res);
251
252        mState = state;
253
254        updateLocalState();
255    }
256
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