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