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