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