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