1 /* 2 * Copyright (C) 2014 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 package androidx.core.graphics.drawable; 17 18 import android.content.res.Resources; 19 import android.graphics.Bitmap; 20 import android.graphics.BitmapShader; 21 import android.graphics.Canvas; 22 import android.graphics.ColorFilter; 23 import android.graphics.Matrix; 24 import android.graphics.Paint; 25 import android.graphics.PixelFormat; 26 import android.graphics.Rect; 27 import android.graphics.RectF; 28 import android.graphics.Shader; 29 import android.graphics.drawable.Drawable; 30 import android.util.DisplayMetrics; 31 import android.view.Gravity; 32 33 import org.jspecify.annotations.NonNull; 34 import org.jspecify.annotations.Nullable; 35 36 /** 37 * A Drawable that wraps a bitmap and can be drawn with rounded corners. You can create a 38 * RoundedBitmapDrawable from a file path, an input stream, or from a 39 * {@link android.graphics.Bitmap} object. 40 * <p> 41 * Also see the {@link android.graphics.Bitmap} class, which handles the management and 42 * transformation of raw bitmap graphics, and should be used when drawing to a 43 * {@link android.graphics.Canvas}. 44 * </p> 45 */ 46 public abstract class RoundedBitmapDrawable extends Drawable { 47 private static final int DEFAULT_PAINT_FLAGS = 48 Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG; 49 final Bitmap mBitmap; 50 private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; 51 private int mGravity = Gravity.FILL; 52 private final Paint mPaint = new Paint(DEFAULT_PAINT_FLAGS); 53 private final BitmapShader mBitmapShader; 54 private final Matrix mShaderMatrix = new Matrix(); 55 private float mCornerRadius; 56 57 final Rect mDstRect = new Rect(); // Gravity.apply() sets this 58 private final RectF mDstRectF = new RectF(); 59 60 private boolean mApplyGravity = true; 61 private boolean mIsCircular; 62 63 // These are scaled to match the target density. 64 private int mBitmapWidth; 65 private int mBitmapHeight; 66 67 /** 68 * Returns the paint used to render this drawable. 69 */ getPaint()70 public final @NonNull Paint getPaint() { 71 return mPaint; 72 } 73 74 /** 75 * Returns the bitmap used by this drawable to render. May be null. 76 */ getBitmap()77 public final @Nullable Bitmap getBitmap() { 78 return mBitmap; 79 } 80 computeBitmapSize()81 private void computeBitmapSize() { 82 mBitmapWidth = mBitmap.getScaledWidth(mTargetDensity); 83 mBitmapHeight = mBitmap.getScaledHeight(mTargetDensity); 84 } 85 86 /** 87 * Set the density scale at which this drawable will be rendered. This 88 * method assumes the drawable will be rendered at the same density as the 89 * specified canvas. 90 * 91 * @param canvas The Canvas from which the density scale must be obtained. 92 * 93 * @see android.graphics.Bitmap#setDensity(int) 94 * @see android.graphics.Bitmap#getDensity() 95 */ setTargetDensity(@onNull Canvas canvas)96 public void setTargetDensity(@NonNull Canvas canvas) { 97 setTargetDensity(canvas.getDensity()); 98 } 99 100 /** 101 * Set the density scale at which this drawable will be rendered. 102 * 103 * @param metrics The DisplayMetrics indicating the density scale for this drawable. 104 * 105 * @see android.graphics.Bitmap#setDensity(int) 106 * @see android.graphics.Bitmap#getDensity() 107 */ setTargetDensity(@onNull DisplayMetrics metrics)108 public void setTargetDensity(@NonNull DisplayMetrics metrics) { 109 setTargetDensity(metrics.densityDpi); 110 } 111 112 /** 113 * Set the density at which this drawable will be rendered. 114 * 115 * @param density The density scale for this drawable. 116 * 117 * @see android.graphics.Bitmap#setDensity(int) 118 * @see android.graphics.Bitmap#getDensity() 119 */ setTargetDensity(int density)120 public void setTargetDensity(int density) { 121 if (mTargetDensity != density) { 122 mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; 123 if (mBitmap != null) { 124 computeBitmapSize(); 125 } 126 invalidateSelf(); 127 } 128 } 129 130 /** 131 * Get the gravity used to position/stretch the bitmap within its bounds. 132 * 133 * @return the gravity applied to the bitmap 134 * 135 * @see android.view.Gravity 136 */ getGravity()137 public int getGravity() { 138 return mGravity; 139 } 140 141 /** 142 * Set the gravity used to position/stretch the bitmap within its bounds. 143 * 144 * @param gravity the gravity 145 * 146 * @see android.view.Gravity 147 */ setGravity(int gravity)148 public void setGravity(int gravity) { 149 if (mGravity != gravity) { 150 mGravity = gravity; 151 mApplyGravity = true; 152 invalidateSelf(); 153 } 154 } 155 156 /** 157 * Enables or disables the mipmap hint for this drawable's bitmap. 158 * See {@link Bitmap#setHasMipMap(boolean)} for more information. 159 * 160 * If the bitmap is null, or the current API version does not support setting a mipmap hint, 161 * calling this method has no effect. 162 * 163 * @param mipMap True if the bitmap should use mipmaps, false otherwise. 164 * 165 * @see #hasMipMap() 166 */ setMipMap(boolean mipMap)167 public void setMipMap(boolean mipMap) { 168 throw new UnsupportedOperationException(); // must be overridden in subclasses 169 } 170 171 /** 172 * Indicates whether the mipmap hint is enabled on this drawable's bitmap. 173 * 174 * @return True if the mipmap hint is set, false otherwise. If the bitmap 175 * is null, this method always returns false. 176 * 177 * @see #setMipMap(boolean) 178 */ hasMipMap()179 public boolean hasMipMap() { 180 throw new UnsupportedOperationException(); // must be overridden in subclasses 181 } 182 183 /** 184 * Enables or disables anti-aliasing for this drawable. Anti-aliasing affects 185 * the edges of the bitmap only so it applies only when the drawable is rotated. 186 * 187 * @param aa True if the bitmap should be anti-aliased, false otherwise. 188 * 189 * @see #hasAntiAlias() 190 */ setAntiAlias(boolean aa)191 public void setAntiAlias(boolean aa) { 192 mPaint.setAntiAlias(aa); 193 invalidateSelf(); 194 } 195 196 /** 197 * Indicates whether anti-aliasing is enabled for this drawable. 198 * 199 * @return True if anti-aliasing is enabled, false otherwise. 200 * 201 * @see #setAntiAlias(boolean) 202 */ hasAntiAlias()203 public boolean hasAntiAlias() { 204 return mPaint.isAntiAlias(); 205 } 206 207 @Override setFilterBitmap(boolean filter)208 public void setFilterBitmap(boolean filter) { 209 mPaint.setFilterBitmap(filter); 210 invalidateSelf(); 211 } 212 213 @Override setDither(boolean dither)214 public void setDither(boolean dither) { 215 mPaint.setDither(dither); 216 invalidateSelf(); 217 } 218 gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight, Rect bounds, Rect outRect)219 void gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight, 220 Rect bounds, Rect outRect) { 221 throw new UnsupportedOperationException(); 222 } 223 updateDstRect()224 void updateDstRect() { 225 if (mApplyGravity) { 226 if (mIsCircular) { 227 final int minDimen = Math.min(mBitmapWidth, mBitmapHeight); 228 gravityCompatApply(mGravity, minDimen, minDimen, getBounds(), mDstRect); 229 230 // inset the drawing rectangle to the largest contained square, 231 // so that a circle will be drawn 232 final int minDrawDimen = Math.min(mDstRect.width(), mDstRect.height()); 233 final int insetX = Math.max(0, (mDstRect.width() - minDrawDimen) / 2); 234 final int insetY = Math.max(0, (mDstRect.height() - minDrawDimen) / 2); 235 mDstRect.inset(insetX, insetY); 236 mCornerRadius = 0.5f * minDrawDimen; 237 } else { 238 gravityCompatApply(mGravity, mBitmapWidth, mBitmapHeight, getBounds(), mDstRect); 239 } 240 mDstRectF.set(mDstRect); 241 242 if (mBitmapShader != null) { 243 // setup shader matrix 244 mShaderMatrix.setTranslate(mDstRectF.left,mDstRectF.top); 245 mShaderMatrix.preScale( 246 mDstRectF.width() / mBitmap.getWidth(), 247 mDstRectF.height() / mBitmap.getHeight()); 248 mBitmapShader.setLocalMatrix(mShaderMatrix); 249 mPaint.setShader(mBitmapShader); 250 } 251 252 mApplyGravity = false; 253 } 254 } 255 256 @Override draw(@onNull Canvas canvas)257 public void draw(@NonNull Canvas canvas) { 258 final Bitmap bitmap = mBitmap; 259 if (bitmap == null) { 260 return; 261 } 262 263 updateDstRect(); 264 if (mPaint.getShader() == null) { 265 canvas.drawBitmap(bitmap, null, mDstRect, mPaint); 266 } else { 267 canvas.drawRoundRect(mDstRectF, mCornerRadius, mCornerRadius, mPaint); 268 } 269 } 270 271 @Override setAlpha(int alpha)272 public void setAlpha(int alpha) { 273 final int oldAlpha = mPaint.getAlpha(); 274 if (alpha != oldAlpha) { 275 mPaint.setAlpha(alpha); 276 invalidateSelf(); 277 } 278 } 279 280 @Override getAlpha()281 public int getAlpha() { 282 return mPaint.getAlpha(); 283 } 284 285 @Override setColorFilter(ColorFilter cf)286 public void setColorFilter(ColorFilter cf) { 287 mPaint.setColorFilter(cf); 288 invalidateSelf(); 289 } 290 291 @Override getColorFilter()292 public ColorFilter getColorFilter() { 293 return mPaint.getColorFilter(); 294 } 295 296 /** 297 * Sets the image shape to circular. 298 * <p>This overwrites any calls made to {@link #setCornerRadius(float)} so far.</p> 299 */ setCircular(boolean circular)300 public void setCircular(boolean circular) { 301 mIsCircular = circular; 302 mApplyGravity = true; 303 if (circular) { 304 updateCircularCornerRadius(); 305 mPaint.setShader(mBitmapShader); 306 invalidateSelf(); 307 } else { 308 setCornerRadius(0); 309 } 310 } 311 updateCircularCornerRadius()312 private void updateCircularCornerRadius() { 313 final int minCircularSize = Math.min(mBitmapHeight, mBitmapWidth); 314 mCornerRadius = minCircularSize / 2; 315 } 316 317 /** 318 * @return <code>true</code> if the image is circular, else <code>false</code>. 319 */ isCircular()320 public boolean isCircular() { 321 return mIsCircular; 322 } 323 324 /** 325 * Sets the corner radius to be applied when drawing the bitmap. 326 */ setCornerRadius(float cornerRadius)327 public void setCornerRadius(float cornerRadius) { 328 if (mCornerRadius == cornerRadius) return; 329 330 mIsCircular = false; 331 if (isGreaterThanZero(cornerRadius)) { 332 mPaint.setShader(mBitmapShader); 333 } else { 334 mPaint.setShader(null); 335 } 336 337 mCornerRadius = cornerRadius; 338 invalidateSelf(); 339 } 340 341 @Override onBoundsChange(@onNull Rect bounds)342 protected void onBoundsChange(@NonNull Rect bounds) { 343 super.onBoundsChange(bounds); 344 if (mIsCircular) { 345 updateCircularCornerRadius(); 346 } 347 mApplyGravity = true; 348 } 349 350 /** 351 * @return The corner radius applied when drawing the bitmap. 352 */ getCornerRadius()353 public float getCornerRadius() { 354 return mCornerRadius; 355 } 356 357 @Override getIntrinsicWidth()358 public int getIntrinsicWidth() { 359 return mBitmapWidth; 360 } 361 362 @Override getIntrinsicHeight()363 public int getIntrinsicHeight() { 364 return mBitmapHeight; 365 } 366 367 @Override getOpacity()368 public int getOpacity() { 369 if (mGravity != Gravity.FILL || mIsCircular) { 370 return PixelFormat.TRANSLUCENT; 371 } 372 Bitmap bm = mBitmap; 373 return (bm == null 374 || bm.hasAlpha() 375 || mPaint.getAlpha() < 255 376 || isGreaterThanZero(mCornerRadius)) 377 ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; 378 } 379 RoundedBitmapDrawable(Resources res, Bitmap bitmap)380 RoundedBitmapDrawable(Resources res, Bitmap bitmap) { 381 if (res != null) { 382 mTargetDensity = res.getDisplayMetrics().densityDpi; 383 } 384 385 mBitmap = bitmap; 386 if (mBitmap != null) { 387 computeBitmapSize(); 388 mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 389 } else { 390 mBitmapWidth = mBitmapHeight = -1; 391 mBitmapShader = null; 392 } 393 } 394 isGreaterThanZero(float toCompare)395 private static boolean isGreaterThanZero(float toCompare) { 396 return toCompare > 0.05f; 397 } 398 } 399