• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.support.v4.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.support.annotation.RequiresApi;
31 import android.util.DisplayMetrics;
32 import 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)
45 public 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      */
getPaint()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      */
getBitmap()76     public final Bitmap getBitmap() {
77         return mBitmap;
78     }
79 
computeBitmapSize()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      */
setTargetDensity(Canvas canvas)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      */
setTargetDensity(DisplayMetrics metrics)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      */
setTargetDensity(int density)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      */
getGravity()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      */
setGravity(int gravity)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      */
setMipMap(boolean mipMap)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      */
hasMipMap()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      */
setAntiAlias(boolean aa)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      */
hasAntiAlias()202     public boolean hasAntiAlias() {
203         return mPaint.isAntiAlias();
204     }
205 
206     @Override
setFilterBitmap(boolean filter)207     public void setFilterBitmap(boolean filter) {
208         mPaint.setFilterBitmap(filter);
209         invalidateSelf();
210     }
211 
212     @Override
setDither(boolean dither)213     public void setDither(boolean dither) {
214         mPaint.setDither(dither);
215         invalidateSelf();
216     }
217 
gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight, Rect bounds, Rect outRect)218     void gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight,
219             Rect bounds, Rect outRect) {
220         throw new UnsupportedOperationException();
221     }
222 
updateDstRect()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
draw(Canvas canvas)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
setAlpha(int alpha)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
getAlpha()280     public int getAlpha() {
281         return mPaint.getAlpha();
282     }
283 
284     @Override
setColorFilter(ColorFilter cf)285     public void setColorFilter(ColorFilter cf) {
286         mPaint.setColorFilter(cf);
287         invalidateSelf();
288     }
289 
290     @Override
getColorFilter()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      */
setCircular(boolean circular)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 
updateCircularCornerRadius()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      */
isCircular()319     public boolean isCircular() {
320         return mIsCircular;
321     }
322 
323     /**
324      * Sets the corner radius to be applied when drawing the bitmap.
325      */
setCornerRadius(float cornerRadius)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
onBoundsChange(Rect bounds)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      */
getCornerRadius()352     public float getCornerRadius() {
353         return mCornerRadius;
354     }
355 
356     @Override
getIntrinsicWidth()357     public int getIntrinsicWidth() {
358         return mBitmapWidth;
359     }
360 
361     @Override
getIntrinsicHeight()362     public int getIntrinsicHeight() {
363         return mBitmapHeight;
364     }
365 
366     @Override
getOpacity()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 
RoundedBitmapDrawable(Resources res, Bitmap bitmap)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 
isGreaterThanZero(float toCompare)394     private static boolean isGreaterThanZero(float toCompare) {
395         return toCompare > 0.05f;
396     }
397 }
398