RotateDrawable.java revision cda212d79d449468384cc7744878b8c99984059c
1/*
2 * Copyright (C) 2007 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 org.xmlpull.v1.XmlPullParser;
20import org.xmlpull.v1.XmlPullParserException;
21
22import android.graphics.Canvas;
23import android.graphics.ColorFilter;
24import android.graphics.Rect;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.content.res.Resources.Theme;
28import android.util.TypedValue;
29import android.util.AttributeSet;
30import android.util.Log;
31
32import java.io.IOException;
33
34/**
35 * <p>
36 * A Drawable that can rotate another Drawable based on the current level value.
37 * The start and end angles of rotation can be controlled to map any circular
38 * arc to the level values range.
39 * <p>
40 * It can be defined in an XML file with the <code>&lt;rotate&gt;</code> element.
41 * For more information, see the guide to
42 * <a href="{@docRoot}guide/topics/resources/animation-resource.html">Animation Resources</a>.
43 *
44 * @attr ref android.R.styleable#RotateDrawable_visible
45 * @attr ref android.R.styleable#RotateDrawable_fromDegrees
46 * @attr ref android.R.styleable#RotateDrawable_toDegrees
47 * @attr ref android.R.styleable#RotateDrawable_pivotX
48 * @attr ref android.R.styleable#RotateDrawable_pivotY
49 * @attr ref android.R.styleable#RotateDrawable_drawable
50 */
51public class RotateDrawable extends Drawable implements Drawable.Callback {
52    private static final float MAX_LEVEL = 10000.0f;
53
54    private final RotateState mState;
55
56    private boolean mMutated;
57
58    /**
59     * Create a new rotating drawable with an empty state.
60     */
61    public RotateDrawable() {
62        this(null, null);
63    }
64
65    /**
66     * Create a new rotating drawable with the specified state. A copy of
67     * this state is used as the internal state for the newly created
68     * drawable.
69     *
70     * @param rotateState the state for this drawable
71     */
72    private RotateDrawable(RotateState rotateState, Resources res) {
73        mState = new RotateState(rotateState, this, res);
74    }
75
76    @Override
77    public void draw(Canvas canvas) {
78        final RotateState st = mState;
79        final Drawable d = st.mDrawable;
80        final Rect bounds = d.getBounds();
81        final int w = bounds.right - bounds.left;
82        final int h = bounds.bottom - bounds.top;
83        final float px = st.mPivotXRel ? (w * st.mPivotX) : st.mPivotX;
84        final float py = st.mPivotYRel ? (h * st.mPivotY) : st.mPivotY;
85
86        final int saveCount = canvas.save();
87        canvas.rotate(st.mCurrentDegrees, px + bounds.left, py + bounds.top);
88        d.draw(canvas);
89        canvas.restoreToCount(saveCount);
90    }
91
92    /**
93     * Sets the drawable rotated by this RotateDrawable.
94     *
95     * @param drawable The drawable to rotate
96     */
97    public void setDrawable(Drawable drawable) {
98        final Drawable oldDrawable = mState.mDrawable;
99        if (oldDrawable != drawable) {
100            if (oldDrawable != null) {
101                oldDrawable.setCallback(null);
102            }
103            mState.mDrawable = drawable;
104            if (drawable != null) {
105                drawable.setCallback(this);
106            }
107        }
108    }
109
110    /**
111     * @return The drawable rotated by this RotateDrawable
112     */
113    public Drawable getDrawable() {
114        return mState.mDrawable;
115    }
116
117    @Override
118    public int getChangingConfigurations() {
119        return super.getChangingConfigurations()
120                | mState.mChangingConfigurations
121                | mState.mDrawable.getChangingConfigurations();
122    }
123
124    @Override
125    public void setAlpha(int alpha) {
126        mState.mDrawable.setAlpha(alpha);
127    }
128
129    @Override
130    public int getAlpha() {
131        return mState.mDrawable.getAlpha();
132    }
133
134    @Override
135    public void setColorFilter(ColorFilter cf) {
136        mState.mDrawable.setColorFilter(cf);
137    }
138
139    @Override
140    public int getOpacity() {
141        return mState.mDrawable.getOpacity();
142    }
143
144    /**
145     * Sets the start angle for rotation.
146     *
147     * @param fromDegrees Starting angle in degrees
148     *
149     * @see #getFromDegrees()
150     * @attr ref android.R.styleable#RotateDrawable_fromDegrees
151     */
152    public void setFromDegrees(float fromDegrees) {
153        if (mState.mFromDegrees != fromDegrees) {
154            mState.mFromDegrees = fromDegrees;
155            invalidateSelf();
156        }
157    }
158
159    /**
160     * @return The starting angle for rotation in degrees
161     *
162     * @see #setFromDegrees(float)
163     * @attr ref android.R.styleable#RotateDrawable_fromDegrees
164     */
165    public float getFromDegrees() {
166        return mState.mFromDegrees;
167    }
168
169    /**
170     * Sets the end angle for rotation.
171     *
172     * @param toDegrees Ending angle in degrees
173     *
174     * @see #getToDegrees()
175     * @attr ref android.R.styleable#RotateDrawable_toDegrees
176     */
177    public void setToDegrees(float toDegrees) {
178        if (mState.mToDegrees != toDegrees) {
179            mState.mToDegrees = toDegrees;
180            invalidateSelf();
181        }
182    }
183
184    /**
185     * @return The ending angle for rotation in degrees
186     *
187     * @see #setToDegrees(float)
188     * @attr ref android.R.styleable#RotateDrawable_toDegrees
189     */
190    public float getToDegrees() {
191        return mState.mToDegrees;
192    }
193
194    /**
195     * Sets the X position around which the drawable is rotated.
196     *
197     * @param pivotX X position around which to rotate. If the X pivot is
198     *            relative, the position represents a fraction of the drawable
199     *            width. Otherwise, the position represents an absolute value in
200     *            pixels.
201     *
202     * @see #setPivotXRelative(boolean)
203     * @attr ref android.R.styleable#RotateDrawable_pivotX
204     */
205    public void setPivotX(float pivotX) {
206        if (mState.mPivotX == pivotX) {
207            mState.mPivotX = pivotX;
208            invalidateSelf();
209        }
210    }
211
212    /**
213     * @return X position around which to rotate
214     *
215     * @see #setPivotX(float)
216     * @attr ref android.R.styleable#RotateDrawable_pivotX
217     */
218    public float getPivotX() {
219        return mState.mPivotX;
220    }
221
222    /**
223     * Sets whether the X pivot value represents a fraction of the drawable
224     * width or an absolute value in pixels.
225     *
226     * @param relative True if the X pivot represents a fraction of the drawable
227     *            width, or false if it represents an absolute value in pixels
228     *
229     * @see #isPivotXRelative()
230     */
231    public void setPivotXRelative(boolean relative) {
232        if (mState.mPivotXRel == relative) {
233            mState.mPivotXRel = relative;
234            invalidateSelf();
235        }
236    }
237
238    /**
239     * @return True if the X pivot represents a fraction of the drawable width,
240     *         or false if it represents an absolute value in pixels
241     *
242     * @see #setPivotXRelative(boolean)
243     */
244    public boolean isPivotXRelative() {
245        return mState.mPivotXRel;
246    }
247
248    /**
249     * Sets the Y position around which the drawable is rotated.
250     *
251     * @param pivotY Y position around which to rotate. If the Y pivot is
252     *            relative, the position represents a fraction of the drawable
253     *            height. Otherwise, the position represents an absolute value
254     *            in pixels.
255     *
256     * @see #getPivotY()
257     * @attr ref android.R.styleable#RotateDrawable_pivotY
258     */
259    public void setPivotY(float pivotY) {
260        if (mState.mPivotY == pivotY) {
261            mState.mPivotY = pivotY;
262            invalidateSelf();
263        }
264    }
265
266    /**
267     * @return Y position around which to rotate
268     *
269     * @see #setPivotY(float)
270     * @attr ref android.R.styleable#RotateDrawable_pivotY
271     */
272    public float getPivotY() {
273        return mState.mPivotY;
274    }
275
276    /**
277     * Sets whether the Y pivot value represents a fraction of the drawable
278     * height or an absolute value in pixels.
279     *
280     * @param relative True if the Y pivot represents a fraction of the drawable
281     *            height, or false if it represents an absolute value in pixels
282     *
283     * @see #isPivotYRelative()
284     */
285    public void setPivotYRelative(boolean relative) {
286        if (mState.mPivotYRel == relative) {
287            mState.mPivotYRel = relative;
288            invalidateSelf();
289        }
290    }
291
292    /**
293     * @return True if the Y pivot represents a fraction of the drawable height,
294     *         or false if it represents an absolute value in pixels
295     *
296     * @see #setPivotYRelative(boolean)
297     */
298    public boolean isPivotYRelative() {
299        return mState.mPivotYRel;
300    }
301
302    @Override
303    public void invalidateDrawable(Drawable who) {
304        final Callback callback = getCallback();
305        if (callback != null) {
306            callback.invalidateDrawable(this);
307        }
308    }
309
310    @Override
311    public void scheduleDrawable(Drawable who, Runnable what, long when) {
312        final Callback callback = getCallback();
313        if (callback != null) {
314            callback.scheduleDrawable(this, what, when);
315        }
316    }
317
318    @Override
319    public void unscheduleDrawable(Drawable who, Runnable what) {
320        final Callback callback = getCallback();
321        if (callback != null) {
322            callback.unscheduleDrawable(this, what);
323        }
324    }
325
326   @Override
327    public boolean getPadding(Rect padding) {
328        return mState.mDrawable.getPadding(padding);
329    }
330
331    @Override
332    public boolean setVisible(boolean visible, boolean restart) {
333        mState.mDrawable.setVisible(visible, restart);
334        return super.setVisible(visible, restart);
335    }
336
337    @Override
338    public boolean isStateful() {
339        return mState.mDrawable.isStateful();
340    }
341
342    @Override
343    protected boolean onStateChange(int[] state) {
344        final boolean changed = mState.mDrawable.setState(state);
345        onBoundsChange(getBounds());
346        return changed;
347    }
348
349    @Override
350    protected boolean onLevelChange(int level) {
351        mState.mDrawable.setLevel(level);
352        onBoundsChange(getBounds());
353
354        mState.mCurrentDegrees = mState.mFromDegrees +
355                (mState.mToDegrees - mState.mFromDegrees) *
356                        (level / MAX_LEVEL);
357
358        invalidateSelf();
359        return true;
360    }
361
362    @Override
363    protected void onBoundsChange(Rect bounds) {
364        mState.mDrawable.setBounds(bounds.left, bounds.top,
365                bounds.right, bounds.bottom);
366    }
367
368    @Override
369    public int getIntrinsicWidth() {
370        return mState.mDrawable.getIntrinsicWidth();
371    }
372
373    @Override
374    public int getIntrinsicHeight() {
375        return mState.mDrawable.getIntrinsicHeight();
376    }
377
378    @Override
379    public ConstantState getConstantState() {
380        if (mState.canConstantState()) {
381            mState.mChangingConfigurations = getChangingConfigurations();
382            return mState;
383        }
384        return null;
385    }
386
387    @Override
388    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
389            throws XmlPullParserException, IOException {
390        final TypedArray a = r.obtainAttributes(attrs,
391                com.android.internal.R.styleable.RotateDrawable);
392
393        super.inflateWithAttributes(r, parser, a,
394                com.android.internal.R.styleable.RotateDrawable_visible);
395
396        TypedValue tv = a.peekValue(com.android.internal.R.styleable.RotateDrawable_pivotX);
397        final boolean pivotXRel;
398        final float pivotX;
399        if (tv == null) {
400            pivotXRel = true;
401            pivotX = 0.5f;
402        } else {
403            pivotXRel = tv.type == TypedValue.TYPE_FRACTION;
404            pivotX = pivotXRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
405        }
406
407        tv = a.peekValue(com.android.internal.R.styleable.RotateDrawable_pivotY);
408        final boolean pivotYRel;
409        final float pivotY;
410        if (tv == null) {
411            pivotYRel = true;
412            pivotY = 0.5f;
413        } else {
414            pivotYRel = tv.type == TypedValue.TYPE_FRACTION;
415            pivotY = pivotYRel ? tv.getFraction(1.0f, 1.0f) : tv.getFloat();
416        }
417
418        final float fromDegrees = a.getFloat(
419                com.android.internal.R.styleable.RotateDrawable_fromDegrees, 0.0f);
420        final float toDegrees = a.getFloat(
421                com.android.internal.R.styleable.RotateDrawable_toDegrees, 360.0f);
422
423        final int res = a.getResourceId(
424                com.android.internal.R.styleable.RotateDrawable_drawable, 0);
425        Drawable drawable = null;
426        if (res > 0) {
427            drawable = r.getDrawable(res);
428        }
429
430        a.recycle();
431
432        final int outerDepth = parser.getDepth();
433        int type;
434        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT &&
435               (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
436
437            if (type != XmlPullParser.START_TAG) {
438                continue;
439            }
440
441            if ((drawable = Drawable.createFromXmlInner(r, parser, attrs, theme)) == null) {
442                Log.w("drawable", "Bad element under <rotate>: "
443                        + parser .getName());
444            }
445        }
446
447        if (drawable == null) {
448            Log.w("drawable", "No drawable specified for <rotate>");
449        }
450
451        final RotateState st = mState;
452        st.mDrawable = drawable;
453        st.mPivotXRel = pivotXRel;
454        st.mPivotX = pivotX;
455        st.mPivotYRel = pivotYRel;
456        st.mPivotY = pivotY;
457        st.mFromDegrees = fromDegrees;
458        st.mCurrentDegrees = fromDegrees;
459        st.mToDegrees = toDegrees;
460
461        if (drawable != null) {
462            drawable.setCallback(this);
463        }
464    }
465
466    @Override
467    public Drawable mutate() {
468        if (!mMutated && super.mutate() == this) {
469            mState.mDrawable.mutate();
470            mMutated = true;
471        }
472        return this;
473    }
474
475    /**
476     * Represents the state of a rotation for a given drawable. The same
477     * rotate drawable can be invoked with different states to drive several
478     * rotations at the same time.
479     */
480    final static class RotateState extends Drawable.ConstantState {
481        Drawable mDrawable;
482
483        int mChangingConfigurations;
484
485        boolean mPivotXRel;
486        float mPivotX;
487        boolean mPivotYRel;
488        float mPivotY;
489
490        float mFromDegrees;
491        float mToDegrees;
492
493        float mCurrentDegrees;
494
495        private boolean mCanConstantState;
496        private boolean mCheckedConstantState;
497
498        public RotateState(RotateState source, RotateDrawable owner, Resources res) {
499            if (source != null) {
500                if (res != null) {
501                    mDrawable = source.mDrawable.getConstantState().newDrawable(res);
502                } else {
503                    mDrawable = source.mDrawable.getConstantState().newDrawable();
504                }
505                mDrawable.setCallback(owner);
506                mDrawable.setLayoutDirection(source.mDrawable.getLayoutDirection());
507                mPivotXRel = source.mPivotXRel;
508                mPivotX = source.mPivotX;
509                mPivotYRel = source.mPivotYRel;
510                mPivotY = source.mPivotY;
511                mFromDegrees = mCurrentDegrees = source.mFromDegrees;
512                mToDegrees = source.mToDegrees;
513                mCanConstantState = mCheckedConstantState = true;
514            }
515        }
516
517        @Override
518        public Drawable newDrawable() {
519            return new RotateDrawable(this, null);
520        }
521
522        @Override
523        public Drawable newDrawable(Resources res) {
524            return new RotateDrawable(this, res);
525        }
526
527        @Override
528        public int getChangingConfigurations() {
529            return mChangingConfigurations;
530        }
531
532        public boolean canConstantState() {
533            if (!mCheckedConstantState) {
534                mCanConstantState = mDrawable.getConstantState() != null;
535                mCheckedConstantState = true;
536            }
537
538            return mCanConstantState;
539        }
540    }
541}
542