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