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