ShapeDrawable.java revision 446d988b44b6e492e4cfc36be00a1e1aeaaf8c20
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 android.annotation.NonNull;
20import android.content.pm.ActivityInfo.Config;
21import android.content.res.ColorStateList;
22import android.content.res.Resources;
23import android.content.res.Resources.Theme;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.ColorFilter;
27import android.graphics.Outline;
28import android.graphics.Paint;
29import android.graphics.PixelFormat;
30import android.graphics.PorterDuff;
31import android.graphics.PorterDuff.Mode;
32import android.graphics.PorterDuffColorFilter;
33import android.graphics.Rect;
34import android.graphics.Shader;
35import android.graphics.drawable.shapes.Shape;
36import android.util.AttributeSet;
37
38import com.android.internal.R;
39
40import org.xmlpull.v1.XmlPullParser;
41import org.xmlpull.v1.XmlPullParserException;
42
43import java.io.IOException;
44
45/**
46 * A Drawable object that draws primitive shapes. A ShapeDrawable takes a
47 * {@link android.graphics.drawable.shapes.Shape} object and manages its
48 * presence on the screen. If no Shape is given, then the ShapeDrawable will
49 * default to a {@link android.graphics.drawable.shapes.RectShape}.
50 * <p>
51 * This object can be defined in an XML file with the <code>&lt;shape></code>
52 * element.
53 * </p>
54 * <div class="special reference"> <h3>Developer Guides</h3>
55 * <p>
56 * For more information about how to use ShapeDrawable, read the <a
57 * href="{@docRoot}guide/topics/graphics/2d-graphics.html#shape-drawable">
58 * Canvas and Drawables</a> document. For more information about defining a
59 * ShapeDrawable in XML, read the
60 * <a href="{@docRoot}guide/topics/resources/drawable-resource.html#Shape">
61 * Drawable Resources</a> document.
62 * </p>
63 * </div>
64 *
65 * @attr ref android.R.styleable#ShapeDrawablePadding_left
66 * @attr ref android.R.styleable#ShapeDrawablePadding_top
67 * @attr ref android.R.styleable#ShapeDrawablePadding_right
68 * @attr ref android.R.styleable#ShapeDrawablePadding_bottom
69 * @attr ref android.R.styleable#ShapeDrawable_color
70 * @attr ref android.R.styleable#ShapeDrawable_width
71 * @attr ref android.R.styleable#ShapeDrawable_height
72 */
73public class ShapeDrawable extends Drawable {
74    private @NonNull ShapeState mShapeState;
75    private PorterDuffColorFilter mTintFilter;
76    private boolean mMutated;
77
78    /**
79     * ShapeDrawable constructor.
80     */
81    public ShapeDrawable() {
82        this(new ShapeState(), null);
83    }
84
85    /**
86     * Creates a ShapeDrawable with a specified Shape.
87     *
88     * @param s the Shape that this ShapeDrawable should be
89     */
90    public ShapeDrawable(Shape s) {
91        this(new ShapeState(), null);
92
93        mShapeState.mShape = s;
94    }
95
96    /**
97     * Returns the Shape of this ShapeDrawable.
98     */
99    public Shape getShape() {
100        return mShapeState.mShape;
101    }
102
103    /**
104     * Sets the Shape of this ShapeDrawable.
105     */
106    public void setShape(Shape s) {
107        mShapeState.mShape = s;
108        updateShape();
109    }
110
111    /**
112     * Sets a ShaderFactory to which requests for a
113     * {@link android.graphics.Shader} object will be made.
114     *
115     * @param fact an instance of your ShaderFactory implementation
116     */
117    public void setShaderFactory(ShaderFactory fact) {
118        mShapeState.mShaderFactory = fact;
119    }
120
121    /**
122     * Returns the ShaderFactory used by this ShapeDrawable for requesting a
123     * {@link android.graphics.Shader}.
124     */
125    public ShaderFactory getShaderFactory() {
126        return mShapeState.mShaderFactory;
127    }
128
129    /**
130     * Returns the Paint used to draw the shape.
131     */
132    public Paint getPaint() {
133        return mShapeState.mPaint;
134    }
135
136    /**
137     * Sets padding for the shape.
138     *
139     * @param left padding for the left side (in pixels)
140     * @param top padding for the top (in pixels)
141     * @param right padding for the right side (in pixels)
142     * @param bottom padding for the bottom (in pixels)
143     */
144    public void setPadding(int left, int top, int right, int bottom) {
145        if ((left | top | right | bottom) == 0) {
146            mShapeState.mPadding = null;
147        } else {
148            if (mShapeState.mPadding == null) {
149                mShapeState.mPadding = new Rect();
150            }
151            mShapeState.mPadding.set(left, top, right, bottom);
152        }
153        invalidateSelf();
154    }
155
156    /**
157     * Sets padding for this shape, defined by a Rect object. Define the padding
158     * in the Rect object as: left, top, right, bottom.
159     */
160    public void setPadding(Rect padding) {
161        if (padding == null) {
162            mShapeState.mPadding = null;
163        } else {
164            if (mShapeState.mPadding == null) {
165                mShapeState.mPadding = new Rect();
166            }
167            mShapeState.mPadding.set(padding);
168        }
169        invalidateSelf();
170    }
171
172    /**
173     * Sets the intrinsic (default) width for this shape.
174     *
175     * @param width the intrinsic width (in pixels)
176     */
177    public void setIntrinsicWidth(int width) {
178        mShapeState.mIntrinsicWidth = width;
179        invalidateSelf();
180    }
181
182    /**
183     * Sets the intrinsic (default) height for this shape.
184     *
185     * @param height the intrinsic height (in pixels)
186     */
187    public void setIntrinsicHeight(int height) {
188        mShapeState.mIntrinsicHeight = height;
189        invalidateSelf();
190    }
191
192    @Override
193    public int getIntrinsicWidth() {
194        return mShapeState.mIntrinsicWidth;
195    }
196
197    @Override
198    public int getIntrinsicHeight() {
199        return mShapeState.mIntrinsicHeight;
200    }
201
202    @Override
203    public boolean getPadding(Rect padding) {
204        if (mShapeState.mPadding != null) {
205            padding.set(mShapeState.mPadding);
206            return true;
207        } else {
208            return super.getPadding(padding);
209        }
210    }
211
212    private static int modulateAlpha(int paintAlpha, int alpha) {
213        int scale = alpha + (alpha >>> 7); // convert to 0..256
214        return paintAlpha * scale >>> 8;
215    }
216
217    /**
218     * Called from the drawable's draw() method after the canvas has been set to
219     * draw the shape at (0,0). Subclasses can override for special effects such
220     * as multiple layers, stroking, etc.
221     */
222    protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
223        shape.draw(canvas, paint);
224    }
225
226    @Override
227    public void draw(Canvas canvas) {
228        final Rect r = getBounds();
229        final ShapeState state = mShapeState;
230        final Paint paint = state.mPaint;
231
232        final int prevAlpha = paint.getAlpha();
233        paint.setAlpha(modulateAlpha(prevAlpha, state.mAlpha));
234
235        // only draw shape if it may affect output
236        if (paint.getAlpha() != 0 || paint.getXfermode() != null || paint.hasShadowLayer()) {
237            final boolean clearColorFilter;
238            if (mTintFilter != null && paint.getColorFilter() == null) {
239                paint.setColorFilter(mTintFilter);
240                clearColorFilter = true;
241            } else {
242                clearColorFilter = false;
243            }
244
245            if (state.mShape != null) {
246                // need the save both for the translate, and for the (unknown)
247                // Shape
248                final int count = canvas.save();
249                canvas.translate(r.left, r.top);
250                onDraw(state.mShape, canvas, paint);
251                canvas.restoreToCount(count);
252            } else {
253                canvas.drawRect(r, paint);
254            }
255
256            if (clearColorFilter) {
257                paint.setColorFilter(null);
258            }
259        }
260
261        // restore
262        paint.setAlpha(prevAlpha);
263    }
264
265    @Override
266    public @Config int getChangingConfigurations() {
267        return super.getChangingConfigurations() | mShapeState.getChangingConfigurations();
268    }
269
270    /**
271     * Set the alpha level for this drawable [0..255]. Note that this drawable
272     * also has a color in its paint, which has an alpha as well. These two
273     * values are automatically combined during drawing. Thus if the color's
274     * alpha is 75% (i.e. 192) and the drawable's alpha is 50% (i.e. 128), then
275     * the combined alpha that will be used during drawing will be 37.5% (i.e.
276     * 96).
277     */
278    @Override
279    public void setAlpha(int alpha) {
280        mShapeState.mAlpha = alpha;
281        invalidateSelf();
282    }
283
284    @Override
285    public int getAlpha() {
286        return mShapeState.mAlpha;
287    }
288
289    @Override
290    public void setTintList(ColorStateList tint) {
291        mShapeState.mTint = tint;
292        mTintFilter = updateTintFilter(mTintFilter, tint, mShapeState.mTintMode);
293        invalidateSelf();
294    }
295
296    @Override
297    public void setTintMode(PorterDuff.Mode tintMode) {
298        mShapeState.mTintMode = tintMode;
299        mTintFilter = updateTintFilter(mTintFilter, mShapeState.mTint, tintMode);
300        invalidateSelf();
301    }
302
303    @Override
304    public void setColorFilter(ColorFilter colorFilter) {
305        mShapeState.mPaint.setColorFilter(colorFilter);
306        invalidateSelf();
307    }
308
309    @Override
310    public int getOpacity() {
311        if (mShapeState.mShape == null) {
312            final Paint p = mShapeState.mPaint;
313            if (p.getXfermode() == null) {
314                final int alpha = p.getAlpha();
315                if (alpha == 0) {
316                    return PixelFormat.TRANSPARENT;
317                }
318                if (alpha == 255) {
319                    return PixelFormat.OPAQUE;
320                }
321            }
322        }
323        // not sure, so be safe
324        return PixelFormat.TRANSLUCENT;
325    }
326
327    @Override
328    public void setDither(boolean dither) {
329        mShapeState.mPaint.setDither(dither);
330        invalidateSelf();
331    }
332
333    @Override
334    protected void onBoundsChange(Rect bounds) {
335        super.onBoundsChange(bounds);
336        updateShape();
337    }
338
339    @Override
340    protected boolean onStateChange(int[] stateSet) {
341        final ShapeState state = mShapeState;
342        if (state.mTint != null && state.mTintMode != null) {
343            mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
344            return true;
345        }
346        return false;
347    }
348
349    @Override
350    public boolean isStateful() {
351        final ShapeState s = mShapeState;
352        return super.isStateful() || (s.mTint != null && s.mTint.isStateful());
353    }
354
355    /**
356     * Subclasses override this to parse custom subelements. If you handle it,
357     * return true, else return <em>super.inflateTag(...)</em>.
358     */
359    protected boolean inflateTag(String name, Resources r, XmlPullParser parser,
360            AttributeSet attrs) {
361
362        if ("padding".equals(name)) {
363            TypedArray a = r.obtainAttributes(attrs,
364                    com.android.internal.R.styleable.ShapeDrawablePadding);
365            setPadding(
366                    a.getDimensionPixelOffset(
367                            com.android.internal.R.styleable.ShapeDrawablePadding_left, 0),
368                    a.getDimensionPixelOffset(
369                            com.android.internal.R.styleable.ShapeDrawablePadding_top, 0),
370                    a.getDimensionPixelOffset(
371                            com.android.internal.R.styleable.ShapeDrawablePadding_right, 0),
372                    a.getDimensionPixelOffset(
373                            com.android.internal.R.styleable.ShapeDrawablePadding_bottom, 0));
374            a.recycle();
375            return true;
376        }
377
378        return false;
379    }
380
381    @Override
382    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
383            throws XmlPullParserException, IOException {
384        super.inflate(r, parser, attrs, theme);
385
386        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ShapeDrawable);
387        updateStateFromTypedArray(a);
388        a.recycle();
389
390        int type;
391        final int outerDepth = parser.getDepth();
392        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
393                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
394            if (type != XmlPullParser.START_TAG) {
395                continue;
396            }
397
398            final String name = parser.getName();
399            // call our subclass
400            if (!inflateTag(name, r, parser, attrs)) {
401                android.util.Log.w("drawable", "Unknown element: " + name +
402                        " for ShapeDrawable " + this);
403            }
404        }
405
406        // Update local properties.
407        updateLocalState();
408    }
409
410    @Override
411    public void applyTheme(Theme t) {
412        super.applyTheme(t);
413
414        final ShapeState state = mShapeState;
415        if (state == null) {
416            return;
417        }
418
419        if (state.mThemeAttrs != null) {
420            final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ShapeDrawable);
421            updateStateFromTypedArray(a);
422            a.recycle();
423        }
424
425        // Apply theme to contained color state list.
426        if (state.mTint != null && state.mTint.canApplyTheme()) {
427            state.mTint = state.mTint.obtainForTheme(t);
428        }
429
430        // Update local properties.
431        updateLocalState();
432    }
433
434    private void updateStateFromTypedArray(TypedArray a) {
435        final ShapeState state = mShapeState;
436        final Paint paint = state.mPaint;
437
438        // Account for any configuration changes.
439        state.mChangingConfigurations |= a.getChangingConfigurations();
440
441        // Extract the theme attributes, if any.
442        state.mThemeAttrs = a.extractThemeAttrs();
443
444        int color = paint.getColor();
445        color = a.getColor(R.styleable.ShapeDrawable_color, color);
446        paint.setColor(color);
447
448        boolean dither = paint.isDither();
449        dither = a.getBoolean(R.styleable.ShapeDrawable_dither, dither);
450        paint.setDither(dither);
451
452        state.mIntrinsicWidth = (int) a.getDimension(
453                R.styleable.ShapeDrawable_width, state.mIntrinsicWidth);
454        state.mIntrinsicHeight = (int) a.getDimension(
455                R.styleable.ShapeDrawable_height, state.mIntrinsicHeight);
456
457        final int tintMode = a.getInt(R.styleable.ShapeDrawable_tintMode, -1);
458        if (tintMode != -1) {
459            state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN);
460        }
461
462        final ColorStateList tint = a.getColorStateList(R.styleable.ShapeDrawable_tint);
463        if (tint != null) {
464            state.mTint = tint;
465        }
466    }
467
468    private void updateShape() {
469        if (mShapeState.mShape != null) {
470            final Rect r = getBounds();
471            final int w = r.width();
472            final int h = r.height();
473
474            mShapeState.mShape.resize(w, h);
475            if (mShapeState.mShaderFactory != null) {
476                mShapeState.mPaint.setShader(mShapeState.mShaderFactory.resize(w, h));
477            }
478        }
479        invalidateSelf();
480    }
481
482    @Override
483    public void getOutline(Outline outline) {
484        if (mShapeState.mShape != null) {
485            mShapeState.mShape.getOutline(outline);
486            outline.setAlpha(getAlpha() / 255.0f);
487        }
488    }
489
490    @Override
491    public ConstantState getConstantState() {
492        mShapeState.mChangingConfigurations = getChangingConfigurations();
493        return mShapeState;
494    }
495
496    @Override
497    public Drawable mutate() {
498        if (!mMutated && super.mutate() == this) {
499            mShapeState = new ShapeState(mShapeState);
500            updateLocalState();
501            mMutated = true;
502        }
503        return this;
504    }
505
506    /**
507     * @hide
508     */
509    public void clearMutated() {
510        super.clearMutated();
511        mMutated = false;
512    }
513
514    /**
515     * Defines the intrinsic properties of this ShapeDrawable's Shape.
516     */
517    static final class ShapeState extends ConstantState {
518        final @NonNull Paint mPaint;
519
520        @Config int mChangingConfigurations;
521        int[] mThemeAttrs;
522        Shape mShape;
523        ColorStateList mTint;
524        Mode mTintMode = DEFAULT_TINT_MODE;
525        Rect mPadding;
526        int mIntrinsicWidth;
527        int mIntrinsicHeight;
528        int mAlpha = 255;
529        ShaderFactory mShaderFactory;
530
531        /**
532         * Constructs a new ShapeState.
533         */
534        ShapeState() {
535            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
536        }
537
538        /**
539         * Constructs a new ShapeState that contains a deep copy of the
540         * specified ShapeState.
541         *
542         * @param orig the state to create a deep copy of
543         */
544        ShapeState(@NonNull ShapeState orig) {
545            mChangingConfigurations = orig.mChangingConfigurations;
546            mPaint = new Paint(orig.mPaint);
547            mThemeAttrs = orig.mThemeAttrs;
548            if (mShape != null) {
549                try {
550                    mShape = orig.mShape.clone();
551                } catch (CloneNotSupportedException e) {
552                    // Well, at least we tried.
553                    mShape = orig.mShape;
554                }
555            }
556            mTint = orig.mTint;
557            mTintMode = orig.mTintMode;
558            if (orig.mPadding != null) {
559                mPadding = new Rect(orig.mPadding);
560            }
561            mIntrinsicWidth = orig.mIntrinsicWidth;
562            mIntrinsicHeight = orig.mIntrinsicHeight;
563            mAlpha = orig.mAlpha;
564
565            // We don't have any way to clone a shader factory, so hopefully
566            // this class doesn't contain any local state.
567            mShaderFactory = orig.mShaderFactory;
568        }
569
570        @Override
571        public boolean canApplyTheme() {
572            return mThemeAttrs != null
573                    || (mTint != null && mTint.canApplyTheme());
574        }
575
576        @Override
577        public Drawable newDrawable() {
578            return new ShapeDrawable(this, null);
579        }
580
581        @Override
582        public Drawable newDrawable(Resources res) {
583            return new ShapeDrawable(this, res);
584        }
585
586        @Override
587        public @Config int getChangingConfigurations() {
588            return mChangingConfigurations
589                    | (mTint != null ? mTint.getChangingConfigurations() : 0);
590        }
591    }
592
593    /**
594     * The one constructor to rule them all. This is called by all public
595     * constructors to set the state and initialize local properties.
596     */
597    private ShapeDrawable(ShapeState state, Resources res) {
598        mShapeState = state;
599
600        updateLocalState();
601    }
602
603    /**
604     * Initializes local dynamic properties from state. This should be called
605     * after significant state changes, e.g. from the One True Constructor and
606     * after inflating or applying a theme.
607     */
608    private void updateLocalState() {
609        mTintFilter = updateTintFilter(mTintFilter, mShapeState.mTint, mShapeState.mTintMode);
610    }
611
612    /**
613     * Base class defines a factory object that is called each time the drawable
614     * is resized (has a new width or height). Its resize() method returns a
615     * corresponding shader, or null. Implement this class if you'd like your
616     * ShapeDrawable to use a special {@link android.graphics.Shader}, such as a
617     * {@link android.graphics.LinearGradient}.
618     */
619    public static abstract class ShaderFactory {
620        /**
621         * Returns the Shader to be drawn when a Drawable is drawn. The
622         * dimensions of the Drawable are passed because they may be needed to
623         * adjust how the Shader is configured for drawing. This is called by
624         * ShapeDrawable.setShape().
625         *
626         * @param width the width of the Drawable being drawn
627         * @param height the heigh of the Drawable being drawn
628         * @return the Shader to be drawn
629         */
630        public abstract Shader resize(int width, int height);
631    }
632}
633