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