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