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