1/*
2 * Copyright (C) 2006 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
17package android.graphics.drawable;
18
19import android.content.res.Resources;
20import android.content.res.TypedArray;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.graphics.BitmapShader;
24import android.graphics.Canvas;
25import android.graphics.ColorFilter;
26import android.graphics.Paint;
27import android.graphics.PixelFormat;
28import android.graphics.Rect;
29import android.graphics.Shader;
30import android.util.AttributeSet;
31import android.util.DisplayMetrics;
32import android.view.Gravity;
33import org.xmlpull.v1.XmlPullParser;
34import org.xmlpull.v1.XmlPullParserException;
35
36import java.io.IOException;
37
38/**
39 * A Drawable that wraps a bitmap and can be tiled, stretched, or aligned. You can create a
40 * BitmapDrawable from a file path, an input stream, through XML inflation, or from
41 * a {@link android.graphics.Bitmap} object.
42 * <p>It can be defined in an XML file with the <code>&lt;bitmap></code> element.  For more
43 * information, see the guide to <a
44 * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
45 * <p>
46 * Also see the {@link android.graphics.Bitmap} class, which handles the management and
47 * transformation of raw bitmap graphics, and should be used when drawing to a
48 * {@link android.graphics.Canvas}.
49 * </p>
50 *
51 * @attr ref android.R.styleable#BitmapDrawable_src
52 * @attr ref android.R.styleable#BitmapDrawable_antialias
53 * @attr ref android.R.styleable#BitmapDrawable_filter
54 * @attr ref android.R.styleable#BitmapDrawable_dither
55 * @attr ref android.R.styleable#BitmapDrawable_gravity
56 * @attr ref android.R.styleable#BitmapDrawable_tileMode
57 */
58public class BitmapDrawable extends Drawable {
59
60    private static final int DEFAULT_PAINT_FLAGS =
61            Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG;
62    private BitmapState mBitmapState;
63    private Bitmap mBitmap;
64    private int mTargetDensity;
65
66    private final Rect mDstRect = new Rect();   // Gravity.apply() sets this
67
68    private boolean mApplyGravity;
69    private boolean mMutated;
70
71     // These are scaled to match the target density.
72    private int mBitmapWidth;
73    private int mBitmapHeight;
74
75    /**
76     * Create an empty drawable, not dealing with density.
77     * @deprecated Use {@link #BitmapDrawable(Resources)} to ensure
78     * that the drawable has correctly set its target density.
79     */
80    @Deprecated
81    public BitmapDrawable() {
82        mBitmapState = new BitmapState((Bitmap) null);
83    }
84
85    /**
86     * Create an empty drawable, setting initial target density based on
87     * the display metrics of the resources.
88     */
89    @SuppressWarnings({"UnusedParameters"})
90    public BitmapDrawable(Resources res) {
91        mBitmapState = new BitmapState((Bitmap) null);
92        mBitmapState.mTargetDensity = mTargetDensity;
93    }
94
95    /**
96     * Create drawable from a bitmap, not dealing with density.
97     * @deprecated Use {@link #BitmapDrawable(Resources, Bitmap)} to ensure
98     * that the drawable has correctly set its target density.
99     */
100    @Deprecated
101    public BitmapDrawable(Bitmap bitmap) {
102        this(new BitmapState(bitmap), null);
103    }
104
105    /**
106     * Create drawable from a bitmap, setting initial target density based on
107     * the display metrics of the resources.
108     */
109    public BitmapDrawable(Resources res, Bitmap bitmap) {
110        this(new BitmapState(bitmap), res);
111        mBitmapState.mTargetDensity = mTargetDensity;
112    }
113
114    /**
115     * Create a drawable by opening a given file path and decoding the bitmap.
116     * @deprecated Use {@link #BitmapDrawable(Resources, String)} to ensure
117     * that the drawable has correctly set its target density.
118     */
119    @Deprecated
120    public BitmapDrawable(String filepath) {
121        this(new BitmapState(BitmapFactory.decodeFile(filepath)), null);
122        if (mBitmap == null) {
123            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath);
124        }
125    }
126
127    /**
128     * Create a drawable by opening a given file path and decoding the bitmap.
129     */
130    @SuppressWarnings({"UnusedParameters"})
131    public BitmapDrawable(Resources res, String filepath) {
132        this(new BitmapState(BitmapFactory.decodeFile(filepath)), null);
133        mBitmapState.mTargetDensity = mTargetDensity;
134        if (mBitmap == null) {
135            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath);
136        }
137    }
138
139    /**
140     * Create a drawable by decoding a bitmap from the given input stream.
141     * @deprecated Use {@link #BitmapDrawable(Resources, java.io.InputStream)} to ensure
142     * that the drawable has correctly set its target density.
143     */
144    @Deprecated
145    public BitmapDrawable(java.io.InputStream is) {
146        this(new BitmapState(BitmapFactory.decodeStream(is)), null);
147        if (mBitmap == null) {
148            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is);
149        }
150    }
151
152    /**
153     * Create a drawable by decoding a bitmap from the given input stream.
154     */
155    @SuppressWarnings({"UnusedParameters"})
156    public BitmapDrawable(Resources res, java.io.InputStream is) {
157        this(new BitmapState(BitmapFactory.decodeStream(is)), null);
158        mBitmapState.mTargetDensity = mTargetDensity;
159        if (mBitmap == null) {
160            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is);
161        }
162    }
163
164    /**
165     * Returns the paint used to render this drawable.
166     */
167    public final Paint getPaint() {
168        return mBitmapState.mPaint;
169    }
170
171    /**
172     * Returns the bitmap used by this drawable to render. May be null.
173     */
174    public final Bitmap getBitmap() {
175        return mBitmap;
176    }
177
178    private void computeBitmapSize() {
179        mBitmapWidth = mBitmap.getScaledWidth(mTargetDensity);
180        mBitmapHeight = mBitmap.getScaledHeight(mTargetDensity);
181    }
182
183    private void setBitmap(Bitmap bitmap) {
184        if (bitmap != mBitmap) {
185            mBitmap = bitmap;
186            if (bitmap != null) {
187                computeBitmapSize();
188            } else {
189                mBitmapWidth = mBitmapHeight = -1;
190            }
191            invalidateSelf();
192        }
193    }
194
195    /**
196     * Set the density scale at which this drawable will be rendered. This
197     * method assumes the drawable will be rendered at the same density as the
198     * specified canvas.
199     *
200     * @param canvas The Canvas from which the density scale must be obtained.
201     *
202     * @see android.graphics.Bitmap#setDensity(int)
203     * @see android.graphics.Bitmap#getDensity()
204     */
205    public void setTargetDensity(Canvas canvas) {
206        setTargetDensity(canvas.getDensity());
207    }
208
209    /**
210     * Set the density scale at which this drawable will be rendered.
211     *
212     * @param metrics The DisplayMetrics indicating the density scale for this drawable.
213     *
214     * @see android.graphics.Bitmap#setDensity(int)
215     * @see android.graphics.Bitmap#getDensity()
216     */
217    public void setTargetDensity(DisplayMetrics metrics) {
218        setTargetDensity(metrics.densityDpi);
219    }
220
221    /**
222     * Set the density at which this drawable will be rendered.
223     *
224     * @param density The density scale for this drawable.
225     *
226     * @see android.graphics.Bitmap#setDensity(int)
227     * @see android.graphics.Bitmap#getDensity()
228     */
229    public void setTargetDensity(int density) {
230        if (mTargetDensity != density) {
231            mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
232            if (mBitmap != null) {
233                computeBitmapSize();
234            }
235            invalidateSelf();
236        }
237    }
238
239    /** Get the gravity used to position/stretch the bitmap within its bounds.
240     * See android.view.Gravity
241     * @return the gravity applied to the bitmap
242     */
243    public int getGravity() {
244        return mBitmapState.mGravity;
245    }
246
247    /** Set the gravity used to position/stretch the bitmap within its bounds.
248        See android.view.Gravity
249     * @param gravity the gravity
250     */
251    public void setGravity(int gravity) {
252        if (mBitmapState.mGravity != gravity) {
253            mBitmapState.mGravity = gravity;
254            mApplyGravity = true;
255            invalidateSelf();
256        }
257    }
258
259    /**
260     * Enables or disables anti-aliasing for this drawable. Anti-aliasing affects
261     * the edges of the bitmap only so it applies only when the drawable is rotated.
262     *
263     * @param aa True if the bitmap should be anti-aliased, false otherwise.
264     */
265    public void setAntiAlias(boolean aa) {
266        mBitmapState.mPaint.setAntiAlias(aa);
267        invalidateSelf();
268    }
269
270    @Override
271    public void setFilterBitmap(boolean filter) {
272        mBitmapState.mPaint.setFilterBitmap(filter);
273        invalidateSelf();
274    }
275
276    @Override
277    public void setDither(boolean dither) {
278        mBitmapState.mPaint.setDither(dither);
279        invalidateSelf();
280    }
281
282    /**
283     * Indicates the repeat behavior of this drawable on the X axis.
284     *
285     * @return {@link Shader.TileMode#CLAMP} if the bitmap does not repeat,
286     *         {@link Shader.TileMode#REPEAT} or {@link Shader.TileMode#MIRROR} otherwise.
287     */
288    public Shader.TileMode getTileModeX() {
289        return mBitmapState.mTileModeX;
290    }
291
292    /**
293     * Indicates the repeat behavior of this drawable on the Y axis.
294     *
295     * @return {@link Shader.TileMode#CLAMP} if the bitmap does not repeat,
296     *         {@link Shader.TileMode#REPEAT} or {@link Shader.TileMode#MIRROR} otherwise.
297     */
298    public Shader.TileMode getTileModeY() {
299        return mBitmapState.mTileModeY;
300    }
301
302    /**
303     * Sets the repeat behavior of this drawable on the X axis. By default, the drawable
304     * does not repeat its bitmap. Using {@link Shader.TileMode#REPEAT} or
305     * {@link Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled) if the bitmap
306     * is smaller than this drawable.
307     *
308     * @param mode The repeat mode for this drawable.
309     *
310     * @see #setTileModeY(android.graphics.Shader.TileMode)
311     * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode)
312     */
313    public void setTileModeX(Shader.TileMode mode) {
314        setTileModeXY(mode, mBitmapState.mTileModeY);
315    }
316
317    /**
318     * Sets the repeat behavior of this drawable on the Y axis. By default, the drawable
319     * does not repeat its bitmap. Using {@link Shader.TileMode#REPEAT} or
320     * {@link Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled) if the bitmap
321     * is smaller than this drawable.
322     *
323     * @param mode The repeat mode for this drawable.
324     *
325     * @see #setTileModeX(android.graphics.Shader.TileMode)
326     * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode)
327     */
328    public final void setTileModeY(Shader.TileMode mode) {
329        setTileModeXY(mBitmapState.mTileModeX, mode);
330    }
331
332    /**
333     * Sets the repeat behavior of this drawable on both axis. By default, the drawable
334     * does not repeat its bitmap. Using {@link Shader.TileMode#REPEAT} or
335     * {@link Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled) if the bitmap
336     * is smaller than this drawable.
337     *
338     * @param xmode The X repeat mode for this drawable.
339     * @param ymode The Y repeat mode for this drawable.
340     *
341     * @see #setTileModeX(android.graphics.Shader.TileMode)
342     * @see #setTileModeY(android.graphics.Shader.TileMode)
343     */
344    public void setTileModeXY(Shader.TileMode xmode, Shader.TileMode ymode) {
345        final BitmapState state = mBitmapState;
346        if (state.mTileModeX != xmode || state.mTileModeY != ymode) {
347            state.mTileModeX = xmode;
348            state.mTileModeY = ymode;
349            state.mRebuildShader = true;
350            invalidateSelf();
351        }
352    }
353
354    @Override
355    public int getChangingConfigurations() {
356        return super.getChangingConfigurations() | mBitmapState.mChangingConfigurations;
357    }
358
359    @Override
360    protected void onBoundsChange(Rect bounds) {
361        super.onBoundsChange(bounds);
362        mApplyGravity = true;
363    }
364
365    @Override
366    public void draw(Canvas canvas) {
367        Bitmap bitmap = mBitmap;
368        if (bitmap != null) {
369            final BitmapState state = mBitmapState;
370            if (state.mRebuildShader) {
371                Shader.TileMode tmx = state.mTileModeX;
372                Shader.TileMode tmy = state.mTileModeY;
373
374                if (tmx == null && tmy == null) {
375                    state.mPaint.setShader(null);
376                } else {
377                    state.mPaint.setShader(new BitmapShader(bitmap,
378                            tmx == null ? Shader.TileMode.CLAMP : tmx,
379                            tmy == null ? Shader.TileMode.CLAMP : tmy));
380                }
381                state.mRebuildShader = false;
382                copyBounds(mDstRect);
383            }
384
385            Shader shader = state.mPaint.getShader();
386            if (shader == null) {
387                if (mApplyGravity) {
388                    final int layoutDirection = getLayoutDirection();
389                    Gravity.apply(state.mGravity, mBitmapWidth, mBitmapHeight,
390                            getBounds(), mDstRect, layoutDirection);
391                    mApplyGravity = false;
392                }
393                canvas.drawBitmap(bitmap, null, mDstRect, state.mPaint);
394            } else {
395                if (mApplyGravity) {
396                    copyBounds(mDstRect);
397                    mApplyGravity = false;
398                }
399                canvas.drawRect(mDstRect, state.mPaint);
400            }
401        }
402    }
403
404    @Override
405    public void setAlpha(int alpha) {
406        int oldAlpha = mBitmapState.mPaint.getAlpha();
407        if (alpha != oldAlpha) {
408            mBitmapState.mPaint.setAlpha(alpha);
409            invalidateSelf();
410        }
411    }
412
413    @Override
414    public void setColorFilter(ColorFilter cf) {
415        mBitmapState.mPaint.setColorFilter(cf);
416        invalidateSelf();
417    }
418
419    /**
420     * A mutable BitmapDrawable still shares its Bitmap with any other Drawable
421     * that comes from the same resource.
422     *
423     * @return This drawable.
424     */
425    @Override
426    public Drawable mutate() {
427        if (!mMutated && super.mutate() == this) {
428            mBitmapState = new BitmapState(mBitmapState);
429            mMutated = true;
430        }
431        return this;
432    }
433
434    @Override
435    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
436            throws XmlPullParserException, IOException {
437        super.inflate(r, parser, attrs);
438
439        TypedArray a = r.obtainAttributes(attrs, com.android.internal.R.styleable.BitmapDrawable);
440
441        final int id = a.getResourceId(com.android.internal.R.styleable.BitmapDrawable_src, 0);
442        if (id == 0) {
443            throw new XmlPullParserException(parser.getPositionDescription() +
444                    ": <bitmap> requires a valid src attribute");
445        }
446        final Bitmap bitmap = BitmapFactory.decodeResource(r, id);
447        if (bitmap == null) {
448            throw new XmlPullParserException(parser.getPositionDescription() +
449                    ": <bitmap> requires a valid src attribute");
450        }
451        mBitmapState.mBitmap = bitmap;
452        setBitmap(bitmap);
453        setTargetDensity(r.getDisplayMetrics());
454
455        final Paint paint = mBitmapState.mPaint;
456        paint.setAntiAlias(a.getBoolean(com.android.internal.R.styleable.BitmapDrawable_antialias,
457                paint.isAntiAlias()));
458        paint.setFilterBitmap(a.getBoolean(com.android.internal.R.styleable.BitmapDrawable_filter,
459                paint.isFilterBitmap()));
460        paint.setDither(a.getBoolean(com.android.internal.R.styleable.BitmapDrawable_dither,
461                paint.isDither()));
462        setGravity(a.getInt(com.android.internal.R.styleable.BitmapDrawable_gravity, Gravity.FILL));
463        int tileMode = a.getInt(com.android.internal.R.styleable.BitmapDrawable_tileMode, -1);
464        if (tileMode != -1) {
465            switch (tileMode) {
466                case 0:
467                    setTileModeXY(Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
468                    break;
469                case 1:
470                    setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
471                    break;
472                case 2:
473                    setTileModeXY(Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);
474                    break;
475            }
476        }
477
478        a.recycle();
479    }
480
481    @Override
482    public int getIntrinsicWidth() {
483        return mBitmapWidth;
484    }
485
486    @Override
487    public int getIntrinsicHeight() {
488        return mBitmapHeight;
489    }
490
491    @Override
492    public int getOpacity() {
493        if (mBitmapState.mGravity != Gravity.FILL) {
494            return PixelFormat.TRANSLUCENT;
495        }
496        Bitmap bm = mBitmap;
497        return (bm == null || bm.hasAlpha() || mBitmapState.mPaint.getAlpha() < 255) ?
498                PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
499    }
500
501    @Override
502    public final ConstantState getConstantState() {
503        mBitmapState.mChangingConfigurations = getChangingConfigurations();
504        return mBitmapState;
505    }
506
507    final static class BitmapState extends ConstantState {
508        Bitmap mBitmap;
509        int mChangingConfigurations;
510        int mGravity = Gravity.FILL;
511        Paint mPaint = new Paint(DEFAULT_PAINT_FLAGS);
512        Shader.TileMode mTileModeX = null;
513        Shader.TileMode mTileModeY = null;
514        int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
515        boolean mRebuildShader;
516
517        BitmapState(Bitmap bitmap) {
518            mBitmap = bitmap;
519        }
520
521        BitmapState(BitmapState bitmapState) {
522            this(bitmapState.mBitmap);
523            mChangingConfigurations = bitmapState.mChangingConfigurations;
524            mGravity = bitmapState.mGravity;
525            mTileModeX = bitmapState.mTileModeX;
526            mTileModeY = bitmapState.mTileModeY;
527            mTargetDensity = bitmapState.mTargetDensity;
528            mPaint = new Paint(bitmapState.mPaint);
529            mRebuildShader = bitmapState.mRebuildShader;
530        }
531
532        @Override
533        public Drawable newDrawable() {
534            return new BitmapDrawable(this, null);
535        }
536
537        @Override
538        public Drawable newDrawable(Resources res) {
539            return new BitmapDrawable(this, res);
540        }
541
542        @Override
543        public int getChangingConfigurations() {
544            return mChangingConfigurations;
545        }
546    }
547
548    private BitmapDrawable(BitmapState state, Resources res) {
549        mBitmapState = state;
550        if (res != null) {
551            mTargetDensity = res.getDisplayMetrics().densityDpi;
552        } else {
553            mTargetDensity = state.mTargetDensity;
554        }
555        setBitmap(state != null ? state.mBitmap : null);
556    }
557}
558