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