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