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