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