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);
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        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
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
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
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
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    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
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
236        public Drawable newDrawable(Resources res) {
237            return new AnimatedRotateDrawable(this, res);
238        }
239    }
240
241    private AnimatedRotateDrawable(AnimatedRotateState state, Resources res) {
242        super(state, res);
243
244        mState = state;
245
246        updateLocalState();
247    }
248
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