BitmapDrawable.java revision 4f64c048505a432e549ccb756634ecebf28f9e80
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.ColorStateList;
20import android.content.res.Resources;
21import android.content.res.Resources.Theme;
22import android.content.res.TypedArray;
23import android.graphics.Bitmap;
24import android.graphics.BitmapFactory;
25import android.graphics.BitmapShader;
26import android.graphics.Canvas;
27import android.graphics.ColorFilter;
28import android.graphics.Insets;
29import android.graphics.Matrix;
30import android.graphics.Paint;
31import android.graphics.PixelFormat;
32import android.graphics.PorterDuff;
33import android.graphics.PorterDuff.Mode;
34import android.graphics.PorterDuffColorFilter;
35import android.graphics.Rect;
36import android.graphics.Shader;
37import android.graphics.Xfermode;
38import android.util.AttributeSet;
39import android.util.DisplayMetrics;
40import android.util.LayoutDirection;
41import android.view.Gravity;
42
43import com.android.internal.R;
44
45import org.xmlpull.v1.XmlPullParser;
46import org.xmlpull.v1.XmlPullParserException;
47
48import java.io.IOException;
49
50/**
51 * A Drawable that wraps a bitmap and can be tiled, stretched, or aligned. You can create a
52 * BitmapDrawable from a file path, an input stream, through XML inflation, or from
53 * a {@link android.graphics.Bitmap} object.
54 * <p>It can be defined in an XML file with the <code>&lt;bitmap></code> element.  For more
55 * information, see the guide to <a
56 * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
57 * <p>
58 * Also see the {@link android.graphics.Bitmap} class, which handles the management and
59 * transformation of raw bitmap graphics, and should be used when drawing to a
60 * {@link android.graphics.Canvas}.
61 * </p>
62 *
63 * @attr ref android.R.styleable#BitmapDrawable_src
64 * @attr ref android.R.styleable#BitmapDrawable_antialias
65 * @attr ref android.R.styleable#BitmapDrawable_filter
66 * @attr ref android.R.styleable#BitmapDrawable_dither
67 * @attr ref android.R.styleable#BitmapDrawable_gravity
68 * @attr ref android.R.styleable#BitmapDrawable_mipMap
69 * @attr ref android.R.styleable#BitmapDrawable_tileMode
70 */
71public class BitmapDrawable extends Drawable {
72    private static final int DEFAULT_PAINT_FLAGS =
73            Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG;
74
75    // Constants for {@link android.R.styleable#BitmapDrawable_tileMode}.
76    private static final int TILE_MODE_UNDEFINED = -2;
77    private static final int TILE_MODE_DISABLED = -1;
78    private static final int TILE_MODE_CLAMP = 0;
79    private static final int TILE_MODE_REPEAT = 1;
80    private static final int TILE_MODE_MIRROR = 2;
81
82    private final Rect mDstRect = new Rect();   // Gravity.apply() sets this
83
84    private BitmapState mBitmapState;
85    private PorterDuffColorFilter mTintFilter;
86
87    private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
88
89    private boolean mApplyGravity;
90    private boolean mMutated;
91
92     // These are scaled to match the target density.
93    private int mBitmapWidth;
94    private int mBitmapHeight;
95
96    /** Optical insets due to gravity. */
97    private Insets mOpticalInsets = null;
98
99    // Mirroring matrix for using with Shaders
100    private Matrix mMirrorMatrix;
101
102    /**
103     * Create an empty drawable, not dealing with density.
104     * @deprecated Use {@link #BitmapDrawable(android.content.res.Resources, android.graphics.Bitmap)}
105     * instead to specify a bitmap to draw with and ensure the correct density is set.
106     */
107    @Deprecated
108    public BitmapDrawable() {
109        mBitmapState = new BitmapState((Bitmap) null);
110    }
111
112    /**
113     * Create an empty drawable, setting initial target density based on
114     * the display metrics of the resources.
115     *
116     * @deprecated Use {@link #BitmapDrawable(android.content.res.Resources, android.graphics.Bitmap)}
117     * instead to specify a bitmap to draw with.
118     */
119    @SuppressWarnings("unused")
120    @Deprecated
121    public BitmapDrawable(Resources res) {
122        mBitmapState = new BitmapState((Bitmap) null);
123        mBitmapState.mTargetDensity = mTargetDensity;
124    }
125
126    /**
127     * Create drawable from a bitmap, not dealing with density.
128     * @deprecated Use {@link #BitmapDrawable(Resources, Bitmap)} to ensure
129     * that the drawable has correctly set its target density.
130     */
131    @Deprecated
132    public BitmapDrawable(Bitmap bitmap) {
133        this(new BitmapState(bitmap), null, null);
134    }
135
136    /**
137     * Create drawable from a bitmap, setting initial target density based on
138     * the display metrics of the resources.
139     */
140    public BitmapDrawable(Resources res, Bitmap bitmap) {
141        this(new BitmapState(bitmap), res, null);
142        mBitmapState.mTargetDensity = mTargetDensity;
143    }
144
145    /**
146     * Create a drawable by opening a given file path and decoding the bitmap.
147     * @deprecated Use {@link #BitmapDrawable(Resources, String)} to ensure
148     * that the drawable has correctly set its target density.
149     */
150    @Deprecated
151    public BitmapDrawable(String filepath) {
152        this(new BitmapState(BitmapFactory.decodeFile(filepath)), null, null);
153        if (mBitmapState.mBitmap == null) {
154            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath);
155        }
156    }
157
158    /**
159     * Create a drawable by opening a given file path and decoding the bitmap.
160     */
161    @SuppressWarnings("unused")
162    public BitmapDrawable(Resources res, String filepath) {
163        this(new BitmapState(BitmapFactory.decodeFile(filepath)), null, null);
164        mBitmapState.mTargetDensity = mTargetDensity;
165        if (mBitmapState.mBitmap == null) {
166            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + filepath);
167        }
168    }
169
170    /**
171     * Create a drawable by decoding a bitmap from the given input stream.
172     * @deprecated Use {@link #BitmapDrawable(Resources, java.io.InputStream)} to ensure
173     * that the drawable has correctly set its target density.
174     */
175    @Deprecated
176    public BitmapDrawable(java.io.InputStream is) {
177        this(new BitmapState(BitmapFactory.decodeStream(is)), null, null);
178        if (mBitmapState.mBitmap == null) {
179            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is);
180        }
181    }
182
183    /**
184     * Create a drawable by decoding a bitmap from the given input stream.
185     */
186    @SuppressWarnings("unused")
187    public BitmapDrawable(Resources res, java.io.InputStream is) {
188        this(new BitmapState(BitmapFactory.decodeStream(is)), null, null);
189        mBitmapState.mTargetDensity = mTargetDensity;
190        if (mBitmapState.mBitmap == null) {
191            android.util.Log.w("BitmapDrawable", "BitmapDrawable cannot decode " + is);
192        }
193    }
194
195    /**
196     * Returns the paint used to render this drawable.
197     */
198    public final Paint getPaint() {
199        return mBitmapState.mPaint;
200    }
201
202    /**
203     * Returns the bitmap used by this drawable to render. May be null.
204     */
205    public final Bitmap getBitmap() {
206        return mBitmapState.mBitmap;
207    }
208
209    private void computeBitmapSize() {
210        final Bitmap bitmap = mBitmapState.mBitmap;
211        if (bitmap != null) {
212            mBitmapWidth = bitmap.getScaledWidth(mTargetDensity);
213            mBitmapHeight = bitmap.getScaledHeight(mTargetDensity);
214        } else {
215            mBitmapWidth = mBitmapHeight = -1;
216        }
217    }
218
219    private void setBitmap(Bitmap bitmap) {
220        if (mBitmapState.mBitmap != bitmap) {
221            mBitmapState.mBitmap = bitmap;
222            computeBitmapSize();
223            invalidateSelf();
224        }
225    }
226
227    /**
228     * Set the density scale at which this drawable will be rendered. This
229     * method assumes the drawable will be rendered at the same density as the
230     * specified canvas.
231     *
232     * @param canvas The Canvas from which the density scale must be obtained.
233     *
234     * @see android.graphics.Bitmap#setDensity(int)
235     * @see android.graphics.Bitmap#getDensity()
236     */
237    public void setTargetDensity(Canvas canvas) {
238        setTargetDensity(canvas.getDensity());
239    }
240
241    /**
242     * Set the density scale at which this drawable will be rendered.
243     *
244     * @param metrics The DisplayMetrics indicating the density scale for this drawable.
245     *
246     * @see android.graphics.Bitmap#setDensity(int)
247     * @see android.graphics.Bitmap#getDensity()
248     */
249    public void setTargetDensity(DisplayMetrics metrics) {
250        setTargetDensity(metrics.densityDpi);
251    }
252
253    /**
254     * Set the density at which this drawable will be rendered.
255     *
256     * @param density The density scale for this drawable.
257     *
258     * @see android.graphics.Bitmap#setDensity(int)
259     * @see android.graphics.Bitmap#getDensity()
260     */
261    public void setTargetDensity(int density) {
262        if (mTargetDensity != density) {
263            mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
264            if (mBitmapState.mBitmap != null) {
265                computeBitmapSize();
266            }
267            invalidateSelf();
268        }
269    }
270
271    /** Get the gravity used to position/stretch the bitmap within its bounds.
272     * See android.view.Gravity
273     * @return the gravity applied to the bitmap
274     */
275    public int getGravity() {
276        return mBitmapState.mGravity;
277    }
278
279    /** Set the gravity used to position/stretch the bitmap within its bounds.
280        See android.view.Gravity
281     * @param gravity the gravity
282     */
283    public void setGravity(int gravity) {
284        if (mBitmapState.mGravity != gravity) {
285            mBitmapState.mGravity = gravity;
286            mApplyGravity = true;
287            invalidateSelf();
288        }
289    }
290
291    /**
292     * Enables or disables the mipmap hint for this drawable's bitmap.
293     * See {@link Bitmap#setHasMipMap(boolean)} for more information.
294     *
295     * If the bitmap is null calling this method has no effect.
296     *
297     * @param mipMap True if the bitmap should use mipmaps, false otherwise.
298     *
299     * @see #hasMipMap()
300     */
301    public void setMipMap(boolean mipMap) {
302        if (mBitmapState.mBitmap != null) {
303            mBitmapState.mBitmap.setHasMipMap(mipMap);
304            invalidateSelf();
305        }
306    }
307
308    /**
309     * Indicates whether the mipmap hint is enabled on this drawable's bitmap.
310     *
311     * @return True if the mipmap hint is set, false otherwise. If the bitmap
312     *         is null, this method always returns false.
313     *
314     * @see #setMipMap(boolean)
315     * @attr ref android.R.styleable#BitmapDrawable_mipMap
316     */
317    public boolean hasMipMap() {
318        return mBitmapState.mBitmap != null && mBitmapState.mBitmap.hasMipMap();
319    }
320
321    /**
322     * Enables or disables anti-aliasing for this drawable. Anti-aliasing affects
323     * the edges of the bitmap only so it applies only when the drawable is rotated.
324     *
325     * @param aa True if the bitmap should be anti-aliased, false otherwise.
326     *
327     * @see #hasAntiAlias()
328     */
329    public void setAntiAlias(boolean aa) {
330        mBitmapState.mPaint.setAntiAlias(aa);
331        invalidateSelf();
332    }
333
334    /**
335     * Indicates whether anti-aliasing is enabled for this drawable.
336     *
337     * @return True if anti-aliasing is enabled, false otherwise.
338     *
339     * @see #setAntiAlias(boolean)
340     */
341    public boolean hasAntiAlias() {
342        return mBitmapState.mPaint.isAntiAlias();
343    }
344
345    @Override
346    public void setFilterBitmap(boolean filter) {
347        mBitmapState.mPaint.setFilterBitmap(filter);
348        invalidateSelf();
349    }
350
351    @Override
352    public void setDither(boolean dither) {
353        mBitmapState.mPaint.setDither(dither);
354        invalidateSelf();
355    }
356
357    /**
358     * Indicates the repeat behavior of this drawable on the X axis.
359     *
360     * @return {@link android.graphics.Shader.TileMode#CLAMP} if the bitmap does not repeat,
361     *         {@link android.graphics.Shader.TileMode#REPEAT} or
362     *         {@link android.graphics.Shader.TileMode#MIRROR} otherwise.
363     */
364    public Shader.TileMode getTileModeX() {
365        return mBitmapState.mTileModeX;
366    }
367
368    /**
369     * Indicates the repeat behavior of this drawable on the Y axis.
370     *
371     * @return {@link android.graphics.Shader.TileMode#CLAMP} if the bitmap does not repeat,
372     *         {@link android.graphics.Shader.TileMode#REPEAT} or
373     *         {@link android.graphics.Shader.TileMode#MIRROR} otherwise.
374     */
375    public Shader.TileMode getTileModeY() {
376        return mBitmapState.mTileModeY;
377    }
378
379    /**
380     * Sets the repeat behavior of this drawable on the X axis. By default, the drawable
381     * does not repeat its bitmap. Using {@link android.graphics.Shader.TileMode#REPEAT} or
382     * {@link android.graphics.Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled)
383     * if the bitmap is smaller than this drawable.
384     *
385     * @param mode The repeat mode for this drawable.
386     *
387     * @see #setTileModeY(android.graphics.Shader.TileMode)
388     * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode)
389     * @attr ref android.R.styleable#BitmapDrawable_tileModeX
390     */
391    public void setTileModeX(Shader.TileMode mode) {
392        setTileModeXY(mode, mBitmapState.mTileModeY);
393    }
394
395    /**
396     * Sets the repeat behavior of this drawable on the Y axis. By default, the drawable
397     * does not repeat its bitmap. Using {@link android.graphics.Shader.TileMode#REPEAT} or
398     * {@link android.graphics.Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled)
399     * if the bitmap is smaller than this drawable.
400     *
401     * @param mode The repeat mode for this drawable.
402     *
403     * @see #setTileModeX(android.graphics.Shader.TileMode)
404     * @see #setTileModeXY(android.graphics.Shader.TileMode, android.graphics.Shader.TileMode)
405     * @attr ref android.R.styleable#BitmapDrawable_tileModeY
406     */
407    public final void setTileModeY(Shader.TileMode mode) {
408        setTileModeXY(mBitmapState.mTileModeX, mode);
409    }
410
411    /**
412     * Sets the repeat behavior of this drawable on both axis. By default, the drawable
413     * does not repeat its bitmap. Using {@link android.graphics.Shader.TileMode#REPEAT} or
414     * {@link android.graphics.Shader.TileMode#MIRROR} the bitmap can be repeated (or tiled)
415     * if the bitmap is smaller than this drawable.
416     *
417     * @param xmode The X repeat mode for this drawable.
418     * @param ymode The Y repeat mode for this drawable.
419     *
420     * @see #setTileModeX(android.graphics.Shader.TileMode)
421     * @see #setTileModeY(android.graphics.Shader.TileMode)
422     */
423    public void setTileModeXY(Shader.TileMode xmode, Shader.TileMode ymode) {
424        final BitmapState state = mBitmapState;
425        if (state.mTileModeX != xmode || state.mTileModeY != ymode) {
426            state.mTileModeX = xmode;
427            state.mTileModeY = ymode;
428            state.mRebuildShader = true;
429            invalidateSelf();
430        }
431    }
432
433    @Override
434    public void setAutoMirrored(boolean mirrored) {
435        if (mBitmapState.mAutoMirrored != mirrored) {
436            mBitmapState.mAutoMirrored = mirrored;
437            invalidateSelf();
438        }
439    }
440
441    @Override
442    public final boolean isAutoMirrored() {
443        return mBitmapState.mAutoMirrored;
444    }
445
446    @Override
447    public int getChangingConfigurations() {
448        return super.getChangingConfigurations() | mBitmapState.mChangingConfigurations;
449    }
450
451    private boolean needMirroring() {
452        return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
453    }
454
455    private void updateMirrorMatrix(float dx) {
456        if (mMirrorMatrix == null) {
457            mMirrorMatrix = new Matrix();
458        }
459        mMirrorMatrix.setTranslate(dx, 0);
460        mMirrorMatrix.preScale(-1.0f, 1.0f);
461    }
462
463    @Override
464    protected void onBoundsChange(Rect bounds) {
465        mApplyGravity = true;
466
467        final Shader shader = mBitmapState.mPaint.getShader();
468        if (shader != null) {
469            if (needMirroring()) {
470                updateMirrorMatrix(bounds.right - bounds.left);
471                shader.setLocalMatrix(mMirrorMatrix);
472                mBitmapState.mPaint.setShader(shader);
473            } else {
474                if (mMirrorMatrix != null) {
475                    mMirrorMatrix = null;
476                    shader.setLocalMatrix(Matrix.IDENTITY_MATRIX);
477                    mBitmapState.mPaint.setShader(shader);
478                }
479            }
480        }
481    }
482
483    @Override
484    public void draw(Canvas canvas) {
485        final Bitmap bitmap = mBitmapState.mBitmap;
486        if (bitmap == null) {
487            return;
488        }
489
490        final BitmapState state = mBitmapState;
491        final Paint paint = state.mPaint;
492        if (state.mRebuildShader) {
493            final Shader.TileMode tmx = state.mTileModeX;
494            final Shader.TileMode tmy = state.mTileModeY;
495            if (tmx == null && tmy == null) {
496                paint.setShader(null);
497            } else {
498                paint.setShader(new BitmapShader(bitmap,
499                        tmx == null ? Shader.TileMode.CLAMP : tmx,
500                        tmy == null ? Shader.TileMode.CLAMP : tmy));
501            }
502
503            state.mRebuildShader = false;
504            copyBounds(mDstRect);
505        }
506
507        final int restoreAlpha;
508        if (state.mBaseAlpha != 1.0f) {
509            final Paint p = getPaint();
510            restoreAlpha = p.getAlpha();
511            p.setAlpha((int) (restoreAlpha * state.mBaseAlpha + 0.5f));
512        } else {
513            restoreAlpha = -1;
514        }
515
516        final boolean clearColorFilter;
517        if (mTintFilter != null && paint.getColorFilter() == null) {
518            paint.setColorFilter(mTintFilter);
519            clearColorFilter = true;
520        } else {
521            clearColorFilter = false;
522        }
523
524        final Shader shader = paint.getShader();
525        final boolean needMirroring = needMirroring();
526        if (shader == null) {
527            if (mApplyGravity) {
528                applyGravity();
529                mApplyGravity = false;
530            }
531
532            if (needMirroring) {
533                canvas.save();
534                // Mirror the bitmap
535                canvas.translate(mDstRect.right - mDstRect.left, 0);
536                canvas.scale(-1.0f, 1.0f);
537            }
538
539            canvas.drawBitmap(bitmap, null, mDstRect, paint);
540
541            if (needMirroring) {
542                canvas.restore();
543            }
544        } else {
545            if (mApplyGravity) {
546                copyBounds(mDstRect);
547                mApplyGravity = false;
548            }
549
550            if (needMirroring) {
551                // Mirror the bitmap
552                updateMirrorMatrix(mDstRect.right - mDstRect.left);
553                shader.setLocalMatrix(mMirrorMatrix);
554                paint.setShader(shader);
555            } else {
556                if (mMirrorMatrix != null) {
557                    mMirrorMatrix = null;
558                    shader.setLocalMatrix(Matrix.IDENTITY_MATRIX);
559                    paint.setShader(shader);
560                }
561            }
562
563            canvas.drawRect(mDstRect, paint);
564        }
565
566        if (clearColorFilter) {
567            paint.setColorFilter(null);
568        }
569
570        if (restoreAlpha >= 0) {
571            paint.setAlpha(restoreAlpha);
572        }
573    }
574
575    /**
576     * @hide
577     */
578    @Override
579    public Insets getOpticalInsets() {
580        if (mApplyGravity && mBitmapState.mPaint.getShader() == null) {
581            applyGravity();
582            mApplyGravity = false;
583        }
584        return mOpticalInsets == null ? Insets.NONE : mOpticalInsets;
585    }
586
587    private void applyGravity() {
588        final Rect bounds = getBounds();
589        final int layoutDirection = getLayoutDirection();
590        Gravity.apply(mBitmapState.mGravity, mBitmapWidth, mBitmapHeight,
591                bounds, mDstRect, layoutDirection);
592
593        final int left = mDstRect.left - bounds.left;
594        final int top = mDstRect.top - bounds.top;
595        final int right = bounds.right - mDstRect.right;
596        final int bottom = bounds.bottom - mDstRect.bottom;
597        mOpticalInsets = Insets.of(left, top, right, bottom);
598    }
599
600    @Override
601    public void setAlpha(int alpha) {
602        final int oldAlpha = mBitmapState.mPaint.getAlpha();
603        if (alpha != oldAlpha) {
604            mBitmapState.mPaint.setAlpha(alpha);
605            invalidateSelf();
606        }
607    }
608
609    @Override
610    public int getAlpha() {
611        return mBitmapState.mPaint.getAlpha();
612    }
613
614    @Override
615    public void setColorFilter(ColorFilter cf) {
616        mBitmapState.mPaint.setColorFilter(cf);
617        invalidateSelf();
618    }
619
620    @Override
621    public ColorFilter getColorFilter() {
622        return mBitmapState.mPaint.getColorFilter();
623    }
624
625    @Override
626    public void setTint(ColorStateList tint, PorterDuff.Mode tintMode) {
627        final BitmapState state = mBitmapState;
628        state.mTint = tint;
629        state.mTintMode = tintMode;
630
631        mTintFilter = updateTintFilter(mTintFilter, tint, tintMode);
632        invalidateSelf();
633    }
634
635    /**
636     * @hide only needed by a hack within ProgressBar
637     */
638    public ColorStateList getTint() {
639        return mBitmapState.mTint;
640    }
641
642    /**
643     * @hide only needed by a hack within ProgressBar
644     */
645    public Mode getTintMode() {
646        return mBitmapState.mTintMode;
647    }
648
649    /**
650     * @hide Candidate for future API inclusion
651     */
652    @Override
653    public void setXfermode(Xfermode xfermode) {
654        mBitmapState.mPaint.setXfermode(xfermode);
655        invalidateSelf();
656    }
657
658    /**
659     * A mutable BitmapDrawable still shares its Bitmap with any other Drawable
660     * that comes from the same resource.
661     *
662     * @return This drawable.
663     */
664    @Override
665    public Drawable mutate() {
666        if (!mMutated && super.mutate() == this) {
667            mBitmapState = new BitmapState(mBitmapState);
668            mMutated = true;
669        }
670        return this;
671    }
672
673    @Override
674    protected boolean onStateChange(int[] stateSet) {
675        final BitmapState state = mBitmapState;
676        if (state.mTint != null && state.mTintMode != null) {
677            mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
678            return true;
679        }
680        return false;
681    }
682
683    @Override
684    public boolean isStateful() {
685        final BitmapState s = mBitmapState;
686        return super.isStateful() || (s.mTint != null && s.mTint.isStateful());
687    }
688
689    @Override
690    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
691            throws XmlPullParserException, IOException {
692        super.inflate(r, parser, attrs, theme);
693
694        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.BitmapDrawable);
695        updateStateFromTypedArray(a);
696        verifyState(a);
697        a.recycle();
698    }
699
700    /**
701     * Ensures all required attributes are set.
702     *
703     * @throws XmlPullParserException if any required attributes are missing
704     */
705    private void verifyState(TypedArray a) throws XmlPullParserException {
706        final BitmapState state = mBitmapState;
707        if (state.mBitmap == null) {
708            throw new XmlPullParserException(a.getPositionDescription() +
709                    ": <bitmap> requires a valid src attribute");
710        }
711    }
712
713    /**
714     * Updates the constant state from the values in the typed array.
715     */
716    private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
717        final Resources r = a.getResources();
718        final BitmapState state = mBitmapState;
719
720        // Account for any configuration changes.
721        state.mChangingConfigurations |= a.getChangingConfigurations();
722
723        // Extract the theme attributes, if any.
724        state.mThemeAttrs = a.extractThemeAttrs();
725
726        final int srcResId = a.getResourceId(R.styleable.BitmapDrawable_src, 0);
727        if (srcResId != 0) {
728            final Bitmap bitmap = BitmapFactory.decodeResource(r, srcResId);
729            if (bitmap == null) {
730                throw new XmlPullParserException(a.getPositionDescription() +
731                        ": <bitmap> requires a valid src attribute");
732            }
733
734            state.mBitmap = bitmap;
735        }
736
737        state.mTargetDensity = r.getDisplayMetrics().densityDpi;
738
739        final boolean defMipMap = state.mBitmap != null ? state.mBitmap.hasMipMap() : false;
740        setMipMap(a.getBoolean(R.styleable.BitmapDrawable_mipMap, defMipMap));
741
742        state.mAutoMirrored = a.getBoolean(
743                R.styleable.BitmapDrawable_autoMirrored, state.mAutoMirrored);
744        state.mBaseAlpha = a.getFloat(R.styleable.BitmapDrawable_alpha, state.mBaseAlpha);
745
746        final int tintMode = a.getInt(R.styleable.BitmapDrawable_tintMode, -1);
747        if (tintMode != -1) {
748            state.mTintMode = Drawable.parseTintMode(tintMode, Mode.SRC_IN);
749        }
750
751        final ColorStateList tint = a.getColorStateList(R.styleable.BitmapDrawable_tint);
752        if (tint != null) {
753            state.mTint = tint;
754        }
755
756        final Paint paint = mBitmapState.mPaint;
757        paint.setAntiAlias(a.getBoolean(
758                R.styleable.BitmapDrawable_antialias, paint.isAntiAlias()));
759        paint.setFilterBitmap(a.getBoolean(
760                R.styleable.BitmapDrawable_filter, paint.isFilterBitmap()));
761        paint.setDither(a.getBoolean(R.styleable.BitmapDrawable_dither, paint.isDither()));
762
763        setGravity(a.getInt(R.styleable.BitmapDrawable_gravity, state.mGravity));
764
765        final int tileMode = a.getInt(R.styleable.BitmapDrawable_tileMode, TILE_MODE_UNDEFINED);
766        if (tileMode != TILE_MODE_UNDEFINED) {
767            final Shader.TileMode mode = parseTileMode(tileMode);
768            setTileModeXY(mode, mode);
769        }
770
771        final int tileModeX = a.getInt(R.styleable.BitmapDrawable_tileModeX, TILE_MODE_UNDEFINED);
772        if (tileModeX != TILE_MODE_UNDEFINED) {
773            setTileModeX(parseTileMode(tileModeX));
774        }
775
776        final int tileModeY = a.getInt(R.styleable.BitmapDrawable_tileModeY, TILE_MODE_UNDEFINED);
777        if (tileModeY != TILE_MODE_UNDEFINED) {
778            setTileModeY(parseTileMode(tileModeY));
779        }
780
781        // Update local properties.
782        initializeWithState(state, r);
783    }
784
785    @Override
786    public void applyTheme(Theme t) {
787        super.applyTheme(t);
788
789        final BitmapState state = mBitmapState;
790        if (state == null || state.mThemeAttrs == null) {
791            return;
792        }
793
794        final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.BitmapDrawable);
795        try {
796            updateStateFromTypedArray(a);
797        } catch (XmlPullParserException e) {
798            throw new RuntimeException(e);
799        } finally {
800            a.recycle();
801        }
802    }
803
804    private static Shader.TileMode parseTileMode(int tileMode) {
805        switch (tileMode) {
806            case TILE_MODE_CLAMP:
807                return Shader.TileMode.CLAMP;
808            case TILE_MODE_REPEAT:
809                return Shader.TileMode.REPEAT;
810            case TILE_MODE_MIRROR:
811                return Shader.TileMode.MIRROR;
812            default:
813                return null;
814        }
815    }
816
817    @Override
818    public boolean canApplyTheme() {
819        return mBitmapState != null && mBitmapState.mThemeAttrs != null;
820    }
821
822    @Override
823    public int getIntrinsicWidth() {
824        return mBitmapWidth;
825    }
826
827    @Override
828    public int getIntrinsicHeight() {
829        return mBitmapHeight;
830    }
831
832    @Override
833    public int getOpacity() {
834        if (mBitmapState.mGravity != Gravity.FILL) {
835            return PixelFormat.TRANSLUCENT;
836        }
837
838        final Bitmap bitmap = mBitmapState.mBitmap;
839        return (bitmap == null || bitmap.hasAlpha() || mBitmapState.mPaint.getAlpha() < 255) ?
840                PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
841    }
842
843    @Override
844    public final ConstantState getConstantState() {
845        mBitmapState.mChangingConfigurations = getChangingConfigurations();
846        return mBitmapState;
847    }
848
849    final static class BitmapState extends ConstantState {
850        final Paint mPaint;
851
852        // Values loaded during inflation.
853        int[] mThemeAttrs = null;
854        Bitmap mBitmap = null;
855        ColorStateList mTint = null;
856        Mode mTintMode = Mode.SRC_IN;
857        int mGravity = Gravity.FILL;
858        float mBaseAlpha = 1.0f;
859        Shader.TileMode mTileModeX = null;
860        Shader.TileMode mTileModeY = null;
861        int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
862        boolean mAutoMirrored = false;
863
864        int mChangingConfigurations;
865        boolean mRebuildShader;
866
867        BitmapState(Bitmap bitmap) {
868            mBitmap = bitmap;
869            mPaint = new Paint(DEFAULT_PAINT_FLAGS);
870        }
871
872        BitmapState(BitmapState bitmapState) {
873            mBitmap = bitmapState.mBitmap;
874            mTint = bitmapState.mTint;
875            mTintMode = bitmapState.mTintMode;
876            mThemeAttrs = bitmapState.mThemeAttrs;
877            mChangingConfigurations = bitmapState.mChangingConfigurations;
878            mGravity = bitmapState.mGravity;
879            mTileModeX = bitmapState.mTileModeX;
880            mTileModeY = bitmapState.mTileModeY;
881            mTargetDensity = bitmapState.mTargetDensity;
882            mBaseAlpha = bitmapState.mBaseAlpha;
883            mPaint = new Paint(bitmapState.mPaint);
884            mRebuildShader = bitmapState.mRebuildShader;
885            mAutoMirrored = bitmapState.mAutoMirrored;
886        }
887
888        @Override
889        public boolean canApplyTheme() {
890            return mThemeAttrs != null;
891        }
892
893        @Override
894        public Bitmap getBitmap() {
895            return mBitmap;
896        }
897
898        @Override
899        public Drawable newDrawable() {
900            return new BitmapDrawable(this, null, null);
901        }
902
903        @Override
904        public Drawable newDrawable(Resources res) {
905            return new BitmapDrawable(this, res, null);
906        }
907
908        @Override
909        public Drawable newDrawable(Resources res, Theme theme) {
910            return new BitmapDrawable(this, res, theme);
911        }
912
913        @Override
914        public int getChangingConfigurations() {
915            return mChangingConfigurations;
916        }
917    }
918
919    /**
920     * The one constructor to rule them all. This is called by all public
921     * constructors to set the state and initialize local properties.
922     */
923    private BitmapDrawable(BitmapState state, Resources res, Theme theme) {
924        if (theme != null && state.canApplyTheme()) {
925            // If we need to apply a theme, implicitly mutate.
926            mBitmapState = new BitmapState(state);
927            applyTheme(theme);
928        } else {
929            mBitmapState = state;
930        }
931
932        initializeWithState(state, res);
933    }
934
935    /**
936     * Initializes local dynamic properties from state. This should be called
937     * after significant state changes, e.g. from the One True Constructor and
938     * after inflating or applying a theme.
939     */
940    private void initializeWithState(BitmapState state, Resources res) {
941        if (res != null) {
942            mTargetDensity = res.getDisplayMetrics().densityDpi;
943        } else {
944            mTargetDensity = state.mTargetDensity;
945        }
946
947        mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
948        computeBitmapSize();
949    }
950}
951