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