NinePatchDrawable.java revision 813d85b82cb7cbaa5dbe05496d1038caa17a1698
1/* 2 * Copyright (C) 2006 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 17package android.graphics.drawable; 18 19import android.content.res.ColorStateList; 20import android.content.res.Resources; 21import android.content.res.TypedArray; 22import android.graphics.Bitmap; 23import android.graphics.BitmapFactory; 24import android.graphics.Canvas; 25import android.graphics.ColorFilter; 26import android.graphics.Insets; 27import android.graphics.NinePatch; 28import android.graphics.Paint; 29import android.graphics.PixelFormat; 30import android.graphics.PorterDuffColorFilter; 31import android.graphics.Rect; 32import android.graphics.Region; 33import android.graphics.PorterDuff.Mode; 34import android.util.AttributeSet; 35import android.util.DisplayMetrics; 36import android.util.LayoutDirection; 37import android.util.TypedValue; 38 39import org.xmlpull.v1.XmlPullParser; 40import org.xmlpull.v1.XmlPullParserException; 41 42import java.io.IOException; 43import java.io.InputStream; 44 45/** 46 * 47 * A resizeable bitmap, with stretchable areas that you define. This type of image 48 * is defined in a .png file with a special format. 49 * 50 * <div class="special reference"> 51 * <h3>Developer Guides</h3> 52 * <p>For more information about how to use a NinePatchDrawable, read the 53 * <a href="{@docRoot}guide/topics/graphics/2d-graphics.html#nine-patch"> 54 * Canvas and Drawables</a> developer guide. For information about creating a NinePatch image 55 * file using the draw9patch tool, see the 56 * <a href="{@docRoot}guide/developing/tools/draw9patch.html">Draw 9-patch</a> tool guide.</p></div> 57 */ 58public class NinePatchDrawable extends Drawable { 59 // dithering helps a lot, and is pretty cheap, so default is true 60 private static final boolean DEFAULT_DITHER = false; 61 private NinePatchState mNinePatchState; 62 private NinePatch mNinePatch; 63 private PorterDuffColorFilter mTintFilter; 64 private Rect mPadding; 65 private Insets mOpticalInsets = Insets.NONE; 66 private Paint mPaint; 67 private boolean mMutated; 68 69 private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; 70 71 // These are scaled to match the target density. 72 private int mBitmapWidth; 73 private int mBitmapHeight; 74 75 NinePatchDrawable() { 76 } 77 78 /** 79 * Create drawable from raw nine-patch data, not dealing with density. 80 * @deprecated Use {@link #NinePatchDrawable(Resources, Bitmap, byte[], Rect, String)} 81 * to ensure that the drawable has correctly set its target density. 82 */ 83 @Deprecated 84 public NinePatchDrawable(Bitmap bitmap, byte[] chunk, Rect padding, String srcName) { 85 this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding), null); 86 } 87 88 /** 89 * Create drawable from raw nine-patch data, setting initial target density 90 * based on the display metrics of the resources. 91 */ 92 public NinePatchDrawable(Resources res, Bitmap bitmap, byte[] chunk, 93 Rect padding, String srcName) { 94 this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding), res); 95 mNinePatchState.mTargetDensity = mTargetDensity; 96 } 97 98 /** 99 * Create drawable from raw nine-patch data, setting initial target density 100 * based on the display metrics of the resources. 101 * 102 * @hide 103 */ 104 public NinePatchDrawable(Resources res, Bitmap bitmap, byte[] chunk, 105 Rect padding, Rect opticalInsets, String srcName) { 106 this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding, opticalInsets), res); 107 mNinePatchState.mTargetDensity = mTargetDensity; 108 } 109 110 /** 111 * Create drawable from existing nine-patch, not dealing with density. 112 * @deprecated Use {@link #NinePatchDrawable(Resources, NinePatch)} 113 * to ensure that the drawable has correctly set its target density. 114 */ 115 @Deprecated 116 public NinePatchDrawable(NinePatch patch) { 117 this(new NinePatchState(patch, new Rect()), null); 118 } 119 120 /** 121 * Create drawable from existing nine-patch, setting initial target density 122 * based on the display metrics of the resources. 123 */ 124 public NinePatchDrawable(Resources res, NinePatch patch) { 125 this(new NinePatchState(patch, new Rect()), res); 126 mNinePatchState.mTargetDensity = mTargetDensity; 127 } 128 129 private void setNinePatchState(NinePatchState state, Resources res) { 130 mNinePatchState = state; 131 mNinePatch = state.mNinePatch; 132 mPadding = state.mPadding; 133 mTargetDensity = res != null ? res.getDisplayMetrics().densityDpi 134 : state.mTargetDensity; 135 //noinspection PointlessBooleanExpression 136 if (state.mDither != DEFAULT_DITHER) { 137 // avoid calling the setter unless we need to, since it does a 138 // lazy allocation of a paint 139 setDither(state.mDither); 140 } 141 142 if (state.mTint != null) { 143 final int color = state.mTint.getColorForState(getState(), 0); 144 mTintFilter = new PorterDuffColorFilter(color, state.mTintMode); 145 } 146 147 setAutoMirrored(state.mAutoMirrored); 148 149 if (mNinePatch != null) { 150 computeBitmapSize(); 151 } 152 } 153 154 /** 155 * Set the density scale at which this drawable will be rendered. This 156 * method assumes the drawable will be rendered at the same density as the 157 * specified canvas. 158 * 159 * @param canvas The Canvas from which the density scale must be obtained. 160 * 161 * @see android.graphics.Bitmap#setDensity(int) 162 * @see android.graphics.Bitmap#getDensity() 163 */ 164 public void setTargetDensity(Canvas canvas) { 165 setTargetDensity(canvas.getDensity()); 166 } 167 168 /** 169 * Set the density scale at which this drawable will be rendered. 170 * 171 * @param metrics The DisplayMetrics indicating the density scale for this drawable. 172 * 173 * @see android.graphics.Bitmap#setDensity(int) 174 * @see android.graphics.Bitmap#getDensity() 175 */ 176 public void setTargetDensity(DisplayMetrics metrics) { 177 setTargetDensity(metrics.densityDpi); 178 } 179 180 /** 181 * Set the density at which this drawable will be rendered. 182 * 183 * @param density The density scale for this drawable. 184 * 185 * @see android.graphics.Bitmap#setDensity(int) 186 * @see android.graphics.Bitmap#getDensity() 187 */ 188 public void setTargetDensity(int density) { 189 if (density != mTargetDensity) { 190 mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; 191 if (mNinePatch != null) { 192 computeBitmapSize(); 193 } 194 invalidateSelf(); 195 } 196 } 197 198 private static Insets scaleFromDensity(Insets insets, int sdensity, int tdensity) { 199 int left = Bitmap.scaleFromDensity(insets.left, sdensity, tdensity); 200 int top = Bitmap.scaleFromDensity(insets.top, sdensity, tdensity); 201 int right = Bitmap.scaleFromDensity(insets.right, sdensity, tdensity); 202 int bottom = Bitmap.scaleFromDensity(insets.bottom, sdensity, tdensity); 203 return Insets.of(left, top, right, bottom); 204 } 205 206 private void computeBitmapSize() { 207 final int sdensity = mNinePatch.getDensity(); 208 final int tdensity = mTargetDensity; 209 if (sdensity == tdensity) { 210 mBitmapWidth = mNinePatch.getWidth(); 211 mBitmapHeight = mNinePatch.getHeight(); 212 mOpticalInsets = mNinePatchState.mOpticalInsets; 213 } else { 214 mBitmapWidth = Bitmap.scaleFromDensity(mNinePatch.getWidth(), sdensity, tdensity); 215 mBitmapHeight = Bitmap.scaleFromDensity(mNinePatch.getHeight(), sdensity, tdensity); 216 if (mNinePatchState.mPadding != null && mPadding != null) { 217 Rect dest = mPadding; 218 Rect src = mNinePatchState.mPadding; 219 if (dest == src) { 220 mPadding = dest = new Rect(src); 221 } 222 dest.left = Bitmap.scaleFromDensity(src.left, sdensity, tdensity); 223 dest.top = Bitmap.scaleFromDensity(src.top, sdensity, tdensity); 224 dest.right = Bitmap.scaleFromDensity(src.right, sdensity, tdensity); 225 dest.bottom = Bitmap.scaleFromDensity(src.bottom, sdensity, tdensity); 226 } 227 mOpticalInsets = scaleFromDensity(mNinePatchState.mOpticalInsets, sdensity, tdensity); 228 } 229 } 230 231 @Override 232 public void draw(Canvas canvas) { 233 final Rect bounds = getBounds(); 234 235 final boolean clearColorFilter; 236 if (mTintFilter != null && getPaint().getColorFilter() == null) { 237 mPaint.setColorFilter(mTintFilter); 238 clearColorFilter = true; 239 } else { 240 clearColorFilter = false; 241 } 242 243 final boolean needsMirroring = needsMirroring(); 244 if (needsMirroring) { 245 canvas.save(); 246 // Mirror the 9patch 247 canvas.translate(bounds.right - bounds.left, 0); 248 canvas.scale(-1.0f, 1.0f); 249 } 250 251 mNinePatch.draw(canvas, bounds, mPaint); 252 253 if (needsMirroring) { 254 canvas.restore(); 255 } 256 257 if (clearColorFilter) { 258 mPaint.setColorFilter(null); 259 } 260 } 261 262 @Override 263 public int getChangingConfigurations() { 264 return super.getChangingConfigurations() | mNinePatchState.mChangingConfigurations; 265 } 266 267 @Override 268 public boolean getPadding(Rect padding) { 269 if (needsMirroring()) { 270 padding.set(mPadding.right, mPadding.top, mPadding.left, mPadding.bottom); 271 } else { 272 padding.set(mPadding); 273 } 274 return (padding.left | padding.top | padding.right | padding.bottom) != 0; 275 } 276 277 /** 278 * @hide 279 */ 280 @Override 281 public Insets getOpticalInsets() { 282 if (needsMirroring()) { 283 return Insets.of(mOpticalInsets.right, mOpticalInsets.top, mOpticalInsets.right, 284 mOpticalInsets.bottom); 285 } else { 286 return mOpticalInsets; 287 } 288 } 289 290 @Override 291 public void setAlpha(int alpha) { 292 if (mPaint == null && alpha == 0xFF) { 293 // Fast common case -- leave at normal alpha. 294 return; 295 } 296 getPaint().setAlpha(alpha); 297 invalidateSelf(); 298 } 299 300 @Override 301 public int getAlpha() { 302 if (mPaint == null) { 303 // Fast common case -- normal alpha. 304 return 0xFF; 305 } 306 return getPaint().getAlpha(); 307 } 308 309 @Override 310 public void setColorFilter(ColorFilter cf) { 311 if (mPaint == null && cf == null) { 312 // Fast common case -- leave at no color filter. 313 return; 314 } 315 getPaint().setColorFilter(cf); 316 invalidateSelf(); 317 } 318 319 /** 320 * Specifies a tint for this drawable. 321 * <p> 322 * Setting a color filter via {@link #setColorFilter(ColorFilter)} overrides 323 * tint. 324 * 325 * @param tint Color state list to use for tinting this drawable, or null to 326 * clear the tint 327 */ 328 public void setTint(ColorStateList tint) { 329 mNinePatchState.mTint = tint; 330 if (mTintFilter == null) { 331 if (tint != null) { 332 final int color = tint.getColorForState(getState(), 0); 333 mTintFilter = new PorterDuffColorFilter(color, mNinePatchState.mTintMode); 334 } 335 } else { 336 if (tint == null) { 337 mTintFilter = null; 338 } 339 } 340 invalidateSelf(); 341 } 342 343 /** 344 * Returns the tint color for this drawable. 345 * 346 * @return Color state list to use for tinting this drawable, or null if 347 * none set 348 */ 349 public ColorStateList getTint() { 350 return mNinePatchState.mTint; 351 } 352 353 /** 354 * Specifies the blending mode used to apply tint. 355 * 356 * @param tintMode A Porter-Duff blending mode 357 * @hide Pending finalization of supported Modes 358 */ 359 public void setTintMode(Mode tintMode) { 360 mNinePatchState.mTintMode = tintMode; 361 if (mTintFilter != null) { 362 mTintFilter.setMode(tintMode); 363 } 364 invalidateSelf(); 365 } 366 367 /** 368 * Returns the blending mode used to apply tint. 369 * 370 * @return The Porter-Duff blending mode used to apply tint. 371 * @hide Pending finalization of supported Modes 372 */ 373 public Mode getTintMode() { 374 return mNinePatchState.mTintMode; 375 } 376 377 @Override 378 public void setDither(boolean dither) { 379 //noinspection PointlessBooleanExpression 380 if (mPaint == null && dither == DEFAULT_DITHER) { 381 // Fast common case -- leave at default dither. 382 return; 383 } 384 385 getPaint().setDither(dither); 386 invalidateSelf(); 387 } 388 389 @Override 390 public void setAutoMirrored(boolean mirrored) { 391 mNinePatchState.mAutoMirrored = mirrored; 392 } 393 394 private boolean needsMirroring() { 395 return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL; 396 } 397 398 @Override 399 public boolean isAutoMirrored() { 400 return mNinePatchState.mAutoMirrored; 401 } 402 403 @Override 404 public void setFilterBitmap(boolean filter) { 405 getPaint().setFilterBitmap(filter); 406 invalidateSelf(); 407 } 408 409 @Override 410 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) 411 throws XmlPullParserException, IOException { 412 super.inflate(r, parser, attrs); 413 414 TypedArray a = r.obtainAttributes(attrs, com.android.internal.R.styleable.NinePatchDrawable); 415 416 final int id = a.getResourceId(com.android.internal.R.styleable.NinePatchDrawable_src, 0); 417 if (id == 0) { 418 throw new XmlPullParserException(parser.getPositionDescription() + 419 ": <nine-patch> requires a valid src attribute"); 420 } 421 422 final boolean dither = a.getBoolean( 423 com.android.internal.R.styleable.NinePatchDrawable_dither, DEFAULT_DITHER); 424 final BitmapFactory.Options options = new BitmapFactory.Options(); 425 if (dither) { 426 options.inDither = false; 427 } 428 options.inScreenDensity = r.getDisplayMetrics().noncompatDensityDpi; 429 430 final Rect padding = new Rect(); 431 final Rect opticalInsets = new Rect(); 432 Bitmap bitmap = null; 433 434 try { 435 final TypedValue value = new TypedValue(); 436 final InputStream is = r.openRawResource(id, value); 437 438 bitmap = BitmapFactory.decodeResourceStream(r, value, is, padding, options); 439 440 is.close(); 441 } catch (IOException e) { 442 // Ignore 443 } 444 445 if (bitmap == null) { 446 throw new XmlPullParserException(parser.getPositionDescription() + 447 ": <nine-patch> requires a valid src attribute"); 448 } else if (bitmap.getNinePatchChunk() == null) { 449 throw new XmlPullParserException(parser.getPositionDescription() + 450 ": <nine-patch> requires a valid 9-patch source image"); 451 } 452 453 final boolean automirrored = a.getBoolean( 454 com.android.internal.R.styleable.NinePatchDrawable_autoMirrored, false); 455 final NinePatchState ninePatchState = new NinePatchState( 456 new NinePatch(bitmap, bitmap.getNinePatchChunk()), padding, opticalInsets, dither, 457 automirrored); 458 459 final int tintModeValue = a.getInt( 460 com.android.internal.R.styleable.NinePatchDrawable_tintMode, -1); 461 ninePatchState.mTintMode = Drawable.parseTintMode(tintModeValue, Mode.SRC_IN); 462 ninePatchState.mTint = a.getColorStateList( 463 com.android.internal.R.styleable.NinePatchDrawable_tint); 464 if (ninePatchState.mTint != null) { 465 final int color = ninePatchState.mTint.getColorForState(getState(), 0); 466 mTintFilter = new PorterDuffColorFilter(color, ninePatchState.mTintMode); 467 } 468 469 setNinePatchState(ninePatchState, r); 470 471 mNinePatchState.mTargetDensity = mTargetDensity; 472 473 a.recycle(); 474 } 475 476 public Paint getPaint() { 477 if (mPaint == null) { 478 mPaint = new Paint(); 479 mPaint.setDither(DEFAULT_DITHER); 480 } 481 return mPaint; 482 } 483 484 /** 485 * Retrieves the width of the source .png file (before resizing). 486 */ 487 @Override 488 public int getIntrinsicWidth() { 489 return mBitmapWidth; 490 } 491 492 /** 493 * Retrieves the height of the source .png file (before resizing). 494 */ 495 @Override 496 public int getIntrinsicHeight() { 497 return mBitmapHeight; 498 } 499 500 @Override 501 public int getMinimumWidth() { 502 return mBitmapWidth; 503 } 504 505 @Override 506 public int getMinimumHeight() { 507 return mBitmapHeight; 508 } 509 510 /** 511 * Returns a {@link android.graphics.PixelFormat graphics.PixelFormat} 512 * value of OPAQUE or TRANSLUCENT. 513 */ 514 @Override 515 public int getOpacity() { 516 return mNinePatch.hasAlpha() || (mPaint != null && mPaint.getAlpha() < 255) ? 517 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; 518 } 519 520 @Override 521 public Region getTransparentRegion() { 522 return mNinePatch.getTransparentRegion(getBounds()); 523 } 524 525 @Override 526 public ConstantState getConstantState() { 527 mNinePatchState.mChangingConfigurations = getChangingConfigurations(); 528 return mNinePatchState; 529 } 530 531 @Override 532 public Drawable mutate() { 533 if (!mMutated && super.mutate() == this) { 534 mNinePatchState = new NinePatchState(mNinePatchState); 535 mNinePatch = mNinePatchState.mNinePatch; 536 mMutated = true; 537 } 538 return this; 539 } 540 541 @Override 542 protected boolean onStateChange(int[] stateSet) { 543 final ColorStateList tint = mNinePatchState.mTint; 544 if (tint != null) { 545 final int newColor = tint.getColorForState(stateSet, 0); 546 final int oldColor = mTintFilter.getColor(); 547 if (oldColor != newColor) { 548 mTintFilter.setColor(newColor); 549 invalidateSelf(); 550 return true; 551 } 552 } 553 554 return false; 555 } 556 557 @Override 558 public boolean isStateful() { 559 final NinePatchState s = mNinePatchState; 560 return super.isStateful() || (s.mTint != null && s.mTint.isStateful()); 561 } 562 563 final static class NinePatchState extends ConstantState { 564 NinePatch mNinePatch; 565 ColorStateList mTint; 566 Mode mTintMode; 567 Rect mPadding; 568 Insets mOpticalInsets; 569 boolean mDither; 570 int mChangingConfigurations; 571 int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; 572 boolean mAutoMirrored; 573 574 NinePatchState(NinePatch ninePatch, Rect padding) { 575 this(ninePatch, padding, new Rect(), DEFAULT_DITHER, false); 576 } 577 578 NinePatchState(NinePatch ninePatch, Rect padding, Rect opticalInsets) { 579 this(ninePatch, padding, opticalInsets, DEFAULT_DITHER, false); 580 } 581 582 NinePatchState(NinePatch ninePatch, Rect rect, Rect opticalInsets, boolean dither, 583 boolean autoMirror) { 584 mNinePatch = ninePatch; 585 mPadding = rect; 586 mOpticalInsets = Insets.of(opticalInsets); 587 mDither = dither; 588 mAutoMirrored = autoMirror; 589 } 590 591 // Copy constructor 592 593 NinePatchState(NinePatchState state) { 594 // We don't deep-copy any fields because they are all immutable. 595 mNinePatch = state.mNinePatch; 596 mTint = state.mTint; 597 mTintMode = state.mTintMode; 598 mPadding = state.mPadding; 599 mOpticalInsets = state.mOpticalInsets; 600 mDither = state.mDither; 601 mChangingConfigurations = state.mChangingConfigurations; 602 mTargetDensity = state.mTargetDensity; 603 mAutoMirrored = state.mAutoMirrored; 604 } 605 606 @Override 607 public Bitmap getBitmap() { 608 return mNinePatch.getBitmap(); 609 } 610 611 @Override 612 public Drawable newDrawable() { 613 return new NinePatchDrawable(this, null); 614 } 615 616 @Override 617 public Drawable newDrawable(Resources res) { 618 return new NinePatchDrawable(this, res); 619 } 620 621 @Override 622 public int getChangingConfigurations() { 623 return mChangingConfigurations; 624 } 625 } 626 627 private NinePatchDrawable(NinePatchState state, Resources res) { 628 setNinePatchState(state, res); 629 } 630} 631