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