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.BitmapShader; 24 import android.graphics.Canvas; 25 import android.graphics.ColorFilter; 26 import android.graphics.Paint; 27 import android.graphics.PixelFormat; 28 import android.graphics.Rect; 29 import android.graphics.Shader; 30 import android.util.AttributeSet; 31 import android.util.DisplayMetrics; 32 import android.view.Gravity; 33 import org.xmlpull.v1.XmlPullParser; 34 import org.xmlpull.v1.XmlPullParserException; 35 36 import java.io.IOException; 37 38 /** 39 * A Drawable that wraps a bitmap and can be tiled, stretched, or aligned. You can create a 40 * BitmapDrawable from a file path, an input stream, through XML inflation, or from 41 * a {@link android.graphics.Bitmap} object. 42 * <p>It can be defined in an XML file with the <code><bitmap></code> element. For more 43 * information, see the guide to <a 44 * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p> 45 * <p> 46 * Also see the {@link android.graphics.Bitmap} class, which handles the management and 47 * transformation of raw bitmap graphics, and should be used when drawing to a 48 * {@link android.graphics.Canvas}. 49 * </p> 50 * 51 * @attr ref android.R.styleable#BitmapDrawable_src 52 * @attr ref android.R.styleable#BitmapDrawable_antialias 53 * @attr ref android.R.styleable#BitmapDrawable_filter 54 * @attr ref android.R.styleable#BitmapDrawable_dither 55 * @attr ref android.R.styleable#BitmapDrawable_gravity 56 * @attr ref android.R.styleable#BitmapDrawable_mipMap 57 * @attr ref android.R.styleable#BitmapDrawable_tileMode 58 */ 59 public class BitmapDrawable extends Drawable { 60 61 private static final int DEFAULT_PAINT_FLAGS = 62 Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG; 63 private BitmapState mBitmapState; 64 private Bitmap mBitmap; 65 private int mTargetDensity; 66 67 private final Rect mDstRect = new Rect(); // Gravity.apply() sets this 68 69 private boolean mApplyGravity; 70 private boolean mMutated; 71 72 // These are scaled to match the target density. 73 private int mBitmapWidth; 74 private int mBitmapHeight; 75 76 /** 77 * Create an empty drawable, not dealing with density. 78 * @deprecated Use {@link #BitmapDrawable(android.content.res.Resources, android.graphics.Bitmap)} 79 * instead to specify a bitmap to draw with and ensure the correct density is set. 80 */ 81 @Deprecated 82 public BitmapDrawable() { 83 mBitmapState = new BitmapState((Bitmap) null); 84 } 85 86 /** 87 * Create an empty drawable, setting initial target density based on 88 * the display metrics of the resources. 89 * @deprecated Use {@link #BitmapDrawable(android.content.res.Resources, android.graphics.Bitmap)} 90 * instead to specify a bitmap to draw with. 91 */ 92 @Deprecated 93 @SuppressWarnings({"UnusedParameters"}) 94 public BitmapDrawable(Resources res) { 95 mBitmapState = new BitmapState((Bitmap) null); 96 mBitmapState.mTargetDensity = mTargetDensity; 97 } 98 99 /** 100 * Create drawable from a bitmap, not dealing with density. 101 * @deprecated Use {@link #BitmapDrawable(Resources, Bitmap)} to ensure 102 * that the drawable has correctly set its target density. 103 */ 104 @Deprecated 105 public BitmapDrawable(Bitmap bitmap) { 106 this(new BitmapState(bitmap), null); 107 } 108 109 /** 110 * Create drawable from a bitmap, setting initial target density based on 111 * the display metrics of the resources. 112 */ 113 public BitmapDrawable(Resources res, Bitmap bitmap) { 114 this(new BitmapState(bitmap), res); 115 mBitmapState.mTargetDensity = mTargetDensity; 116 } 117 118 /** 119 * Create a drawable by opening a given file path and decoding the bitmap. 120 * @deprecated Use {@link #BitmapDrawable(Resources, String)} to ensure 121 * that the drawable has correctly set its target density. 122 */ 123 @Deprecated 124 public BitmapDrawable(String filepath) { 125 this(new BitmapState(BitmapFactory.decodeFile(filepath)), null); 126 if (mBitmap == null) { 127 android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath); 128 } 129 } 130 131 /** 132 * Create a drawable by opening a given file path and decoding the bitmap. 133 */ 134 @SuppressWarnings({"UnusedParameters"}) 135 public BitmapDrawable(Resources res, String filepath) { 136 this(new BitmapState(BitmapFactory.decodeFile(filepath)), null); 137 mBitmapState.mTargetDensity = mTargetDensity; 138 if (mBitmap == null) { 139 android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath); 140 } 141 } 142 143 /** 144 * Create a drawable by decoding a bitmap from the given input stream. 145 * @deprecated Use {@link #BitmapDrawable(Resources, java.io.InputStream)} to ensure 146 * that the drawable has correctly set its target density. 147 */ 148 @Deprecated 149 public BitmapDrawable(java.io.InputStream is) { 150 this(new BitmapState(BitmapFactory.decodeStream(is)), null); 151 if (mBitmap == null) { 152 android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is); 153 } 154 } 155 156 /** 157 * Create a drawable by decoding a bitmap from the given input stream. 158 */ 159 @SuppressWarnings({"UnusedParameters"}) 160 public BitmapDrawable(Resources res, java.io.InputStream is) { 161 this(new BitmapState(BitmapFactory.decodeStream(is)), null); 162 mBitmapState.mTargetDensity = mTargetDensity; 163 if (mBitmap == null) { 164 android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is); 165 } 166 } 167 168 /** 169 * Returns the paint used to render this drawable. 170 */ 171 public final Paint getPaint() { 172 return mBitmapState.mPaint; 173 } 174 175 /** 176 * Returns the bitmap used by this drawable to render. May be null. 177 */ 178 public final Bitmap getBitmap() { 179 return mBitmap; 180 } 181 182 private void computeBitmapSize() { 183 mBitmapWidth = mBitmap.getScaledWidth(mTargetDensity); 184 mBitmapHeight = mBitmap.getScaledHeight(mTargetDensity); 185 } 186 187 private void setBitmap(Bitmap bitmap) { 188 if (bitmap != mBitmap) { 189 mBitmap = bitmap; 190 if (bitmap != null) { 191 computeBitmapSize(); 192 } else { 193 mBitmapWidth = mBitmapHeight = -1; 194 } 195 invalidateSelf(); 196 } 197 } 198 199 /** 200 * Set the density scale at which this drawable will be rendered. This 201 * method assumes the drawable will be rendered at the same density as the 202 * specified canvas. 203 * 204 * @param canvas The Canvas from which the density scale must be obtained. 205 * 206 * @see android.graphics.Bitmap#setDensity(int) 207 * @see android.graphics.Bitmap#getDensity() 208 */ 209 public void setTargetDensity(Canvas canvas) { 210 setTargetDensity(canvas.getDensity()); 211 } 212 213 /** 214 * Set the density scale at which this drawable will be rendered. 215 * 216 * @param metrics The DisplayMetrics indicating the density scale for this drawable. 217 * 218 * @see android.graphics.Bitmap#setDensity(int) 219 * @see android.graphics.Bitmap#getDensity() 220 */ 221 public void setTargetDensity(DisplayMetrics metrics) { 222 setTargetDensity(metrics.densityDpi); 223 } 224 225 /** 226 * Set the density at which this drawable will be rendered. 227 * 228 * @param density The density scale for this drawable. 229 * 230 * @see android.graphics.Bitmap#setDensity(int) 231 * @see android.graphics.Bitmap#getDensity() 232 */ 233 public void setTargetDensity(int density) { 234 if (mTargetDensity != density) { 235 mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; 236 if (mBitmap != null) { 237 computeBitmapSize(); 238 } 239 invalidateSelf(); 240 } 241 } 242 243 /** Get the gravity used to position/stretch the bitmap within its bounds. 244 * See android.view.Gravity 245 * @return the gravity applied to the bitmap 246 */ 247 public int getGravity() { 248 return mBitmapState.mGravity; 249 } 250 251 /** Set the gravity used to position/stretch the bitmap within its bounds. 252 See android.view.Gravity 253 * @param gravity the gravity 254 */ 255 public void setGravity(int gravity) { 256 if (mBitmapState.mGravity != gravity) { 257 mBitmapState.mGravity = gravity; 258 mApplyGravity = true; 259 invalidateSelf(); 260 } 261 } 262 263 /** 264 * Enables or disables the mipmap hint for this drawable's bitmap. 265 * See {@link Bitmap#setHasMipMap(boolean)} for more information. 266 * 267 * If the bitmap is null calling this method has no effect. 268 * 269 * @param mipMap True if the bitmap should use mipmaps, false otherwise. 270 * 271 * @see #hasMipMap() 272 */ 273 public void setMipMap(boolean mipMap) { 274 if (mBitmapState.mBitmap != null) { 275 mBitmapState.mBitmap.setHasMipMap(mipMap); 276 invalidateSelf(); 277 } 278 } 279 280 /** 281 * Indicates whether the mipmap hint is enabled on this drawable's bitmap. 282 * 283 * @return True if the mipmap hint is set, false otherwise. If the bitmap 284 * is null, this method always returns false. 285 * 286 * @see #setMipMap(boolean) 287 * @attr ref android.R.styleable#BitmapDrawable_mipMap 288 */ 289 public boolean hasMipMap() { 290 return mBitmapState.mBitmap != null && mBitmapState.mBitmap.hasMipMap(); 291 } 292 293 /** 294 * Enables or disables anti-aliasing for this drawable. Anti-aliasing affects 295 * the edges of the bitmap only so it applies only when the drawable is rotated. 296 * 297 * @param aa True if the bitmap should be anti-aliased, false otherwise. 298 * 299 * @see #hasAntiAlias() 300 */ 301 public void setAntiAlias(boolean aa) { 302 mBitmapState.mPaint.setAntiAlias(aa); 303 invalidateSelf(); 304 } 305 306 /** 307 * Indicates whether anti-aliasing is enabled for this drawable. 308 * 309 * @return True if anti-aliasing is enabled, false otherwise. 310 * 311 * @see #setAntiAlias(boolean) 312 */ 313 public boolean hasAntiAlias() { 314 return mBitmapState.mPaint.isAntiAlias(); 315 } 316 317 @Override 318 public void setFilterBitmap(boolean filter) { 319 mBitmapState.mPaint.setFilterBitmap(filter); 320 invalidateSelf(); 321 } 322 323 @Override 324 public void setDither(boolean dither) { 325 mBitmapState.mPaint.setDither(dither); 326 invalidateSelf(); 327 } 328 329 /** 330 * Indicates the repeat behavior of this drawable on the X axis. 331 * 332 * @return {@link Shader.TileMode#CLAMP} if the bitmap does not repeat, 333 * {@link Shader.TileMode#REPEAT} or {@link Shader.TileMode#MIRROR} otherwise. 334 */ 335 public Shader.TileMode getTileModeX() { 336 return mBitmapState.mTileModeX; 337 } 338 339 /** 340 * Indicates the repeat behavior of this drawable on the Y axis. 341 * 342 * @return {@link Shader.TileMode#CLAMP} if the bitmap does not repeat, 343 * {@link Shader.TileMode#REPEAT} or {@link Shader.TileMode#MIRROR} otherwise. 344 */ 345 public Shader.TileMode getTileModeY() { 346 return mBitmapState.mTileModeY; 347 } 348 349 /** 350 * Sets the repeat behavior of this drawable on the X axis. By default, the drawable 351 * does not repeat its bitmap. Using {@link Shader.TileMode#REPEAT} or 352 * {@link Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled) if the bitmap 353 * is smaller than this drawable. 354 * 355 * @param mode The repeat mode for this drawable. 356 * 357 * @see #setTileModeY(android.graphics.Shader.TileMode) 358 * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode) 359 */ 360 public void setTileModeX(Shader.TileMode mode) { 361 setTileModeXY(mode, mBitmapState.mTileModeY); 362 } 363 364 /** 365 * Sets the repeat behavior of this drawable on the Y axis. By default, the drawable 366 * does not repeat its bitmap. Using {@link Shader.TileMode#REPEAT} or 367 * {@link Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled) if the bitmap 368 * is smaller than this drawable. 369 * 370 * @param mode The repeat mode for this drawable. 371 * 372 * @see #setTileModeX(android.graphics.Shader.TileMode) 373 * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode) 374 */ 375 public final void setTileModeY(Shader.TileMode mode) { 376 setTileModeXY(mBitmapState.mTileModeX, mode); 377 } 378 379 /** 380 * Sets the repeat behavior of this drawable on both axis. By default, the drawable 381 * does not repeat its bitmap. Using {@link Shader.TileMode#REPEAT} or 382 * {@link Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled) if the bitmap 383 * is smaller than this drawable. 384 * 385 * @param xmode The X repeat mode for this drawable. 386 * @param ymode The Y repeat mode for this drawable. 387 * 388 * @see #setTileModeX(android.graphics.Shader.TileMode) 389 * @see #setTileModeY(android.graphics.Shader.TileMode) 390 */ 391 public void setTileModeXY(Shader.TileMode xmode, Shader.TileMode ymode) { 392 final BitmapState state = mBitmapState; 393 if (state.mTileModeX != xmode || state.mTileModeY != ymode) { 394 state.mTileModeX = xmode; 395 state.mTileModeY = ymode; 396 state.mRebuildShader = true; 397 invalidateSelf(); 398 } 399 } 400 401 @Override 402 public int getChangingConfigurations() { 403 return super.getChangingConfigurations() | mBitmapState.mChangingConfigurations; 404 } 405 406 @Override 407 protected void onBoundsChange(Rect bounds) { 408 super.onBoundsChange(bounds); 409 mApplyGravity = true; 410 } 411 412 @Override 413 public void draw(Canvas canvas) { 414 Bitmap bitmap = mBitmap; 415 if (bitmap != null) { 416 final BitmapState state = mBitmapState; 417 if (state.mRebuildShader) { 418 Shader.TileMode tmx = state.mTileModeX; 419 Shader.TileMode tmy = state.mTileModeY; 420 421 if (tmx == null && tmy == null) { 422 state.mPaint.setShader(null); 423 } else { 424 state.mPaint.setShader(new BitmapShader(bitmap, 425 tmx == null ? Shader.TileMode.CLAMP : tmx, 426 tmy == null ? Shader.TileMode.CLAMP : tmy)); 427 } 428 state.mRebuildShader = false; 429 copyBounds(mDstRect); 430 } 431 432 Shader shader = state.mPaint.getShader(); 433 if (shader == null) { 434 if (mApplyGravity) { 435 final int layoutDirection = getLayoutDirection(); 436 Gravity.apply(state.mGravity, mBitmapWidth, mBitmapHeight, 437 getBounds(), mDstRect, layoutDirection); 438 mApplyGravity = false; 439 } 440 canvas.drawBitmap(bitmap, null, mDstRect, state.mPaint); 441 } else { 442 if (mApplyGravity) { 443 copyBounds(mDstRect); 444 mApplyGravity = false; 445 } 446 canvas.drawRect(mDstRect, state.mPaint); 447 } 448 } 449 } 450 451 @Override 452 public void setAlpha(int alpha) { 453 int oldAlpha = mBitmapState.mPaint.getAlpha(); 454 if (alpha != oldAlpha) { 455 mBitmapState.mPaint.setAlpha(alpha); 456 invalidateSelf(); 457 } 458 } 459 460 @Override 461 public void setColorFilter(ColorFilter cf) { 462 mBitmapState.mPaint.setColorFilter(cf); 463 invalidateSelf(); 464 } 465 466 /** 467 * A mutable BitmapDrawable still shares its Bitmap with any other Drawable 468 * that comes from the same resource. 469 * 470 * @return This drawable. 471 */ 472 @Override 473 public Drawable mutate() { 474 if (!mMutated && super.mutate() == this) { 475 mBitmapState = new BitmapState(mBitmapState); 476 mMutated = true; 477 } 478 return this; 479 } 480 481 @Override 482 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) 483 throws XmlPullParserException, IOException { 484 super.inflate(r, parser, attrs); 485 486 TypedArray a = r.obtainAttributes(attrs, com.android.internal.R.styleable.BitmapDrawable); 487 488 final int id = a.getResourceId(com.android.internal.R.styleable.BitmapDrawable_src, 0); 489 if (id == 0) { 490 throw new XmlPullParserException(parser.getPositionDescription() + 491 ": <bitmap> requires a valid src attribute"); 492 } 493 final Bitmap bitmap = BitmapFactory.decodeResource(r, id); 494 if (bitmap == null) { 495 throw new XmlPullParserException(parser.getPositionDescription() + 496 ": <bitmap> requires a valid src attribute"); 497 } 498 mBitmapState.mBitmap = bitmap; 499 setBitmap(bitmap); 500 setTargetDensity(r.getDisplayMetrics()); 501 setMipMap(a.getBoolean(com.android.internal.R.styleable.BitmapDrawable_mipMap, 502 bitmap.hasMipMap())); 503 504 final Paint paint = mBitmapState.mPaint; 505 paint.setAntiAlias(a.getBoolean(com.android.internal.R.styleable.BitmapDrawable_antialias, 506 paint.isAntiAlias())); 507 paint.setFilterBitmap(a.getBoolean(com.android.internal.R.styleable.BitmapDrawable_filter, 508 paint.isFilterBitmap())); 509 paint.setDither(a.getBoolean(com.android.internal.R.styleable.BitmapDrawable_dither, 510 paint.isDither())); 511 setGravity(a.getInt(com.android.internal.R.styleable.BitmapDrawable_gravity, Gravity.FILL)); 512 int tileMode = a.getInt(com.android.internal.R.styleable.BitmapDrawable_tileMode, -1); 513 if (tileMode != -1) { 514 switch (tileMode) { 515 case 0: 516 setTileModeXY(Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 517 break; 518 case 1: 519 setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); 520 break; 521 case 2: 522 setTileModeXY(Shader.TileMode.MIRROR, Shader.TileMode.MIRROR); 523 break; 524 } 525 } 526 527 a.recycle(); 528 } 529 530 @Override 531 public int getIntrinsicWidth() { 532 return mBitmapWidth; 533 } 534 535 @Override 536 public int getIntrinsicHeight() { 537 return mBitmapHeight; 538 } 539 540 @Override 541 public int getOpacity() { 542 if (mBitmapState.mGravity != Gravity.FILL) { 543 return PixelFormat.TRANSLUCENT; 544 } 545 Bitmap bm = mBitmap; 546 return (bm == null || bm.hasAlpha() || mBitmapState.mPaint.getAlpha() < 255) ? 547 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; 548 } 549 550 @Override 551 public final ConstantState getConstantState() { 552 mBitmapState.mChangingConfigurations = getChangingConfigurations(); 553 return mBitmapState; 554 } 555 556 final static class BitmapState extends ConstantState { 557 Bitmap mBitmap; 558 int mChangingConfigurations; 559 int mGravity = Gravity.FILL; 560 Paint mPaint = new Paint(DEFAULT_PAINT_FLAGS); 561 Shader.TileMode mTileModeX = null; 562 Shader.TileMode mTileModeY = null; 563 int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; 564 boolean mRebuildShader; 565 566 BitmapState(Bitmap bitmap) { 567 mBitmap = bitmap; 568 } 569 570 BitmapState(BitmapState bitmapState) { 571 this(bitmapState.mBitmap); 572 mChangingConfigurations = bitmapState.mChangingConfigurations; 573 mGravity = bitmapState.mGravity; 574 mTileModeX = bitmapState.mTileModeX; 575 mTileModeY = bitmapState.mTileModeY; 576 mTargetDensity = bitmapState.mTargetDensity; 577 mPaint = new Paint(bitmapState.mPaint); 578 mRebuildShader = bitmapState.mRebuildShader; 579 } 580 581 @Override 582 public Drawable newDrawable() { 583 return new BitmapDrawable(this, null); 584 } 585 586 @Override 587 public Drawable newDrawable(Resources res) { 588 return new BitmapDrawable(this, res); 589 } 590 591 @Override 592 public int getChangingConfigurations() { 593 return mChangingConfigurations; 594 } 595 } 596 597 private BitmapDrawable(BitmapState state, Resources res) { 598 mBitmapState = state; 599 if (res != null) { 600 mTargetDensity = res.getDisplayMetrics().densityDpi; 601 } else { 602 mTargetDensity = state.mTargetDensity; 603 } 604 setBitmap(state != null ? state.mBitmap : null); 605 } 606 } 607