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><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