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