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