Home | History | Annotate | Download | only in drawable
      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 
     17 package android.graphics.drawable;
     18 
     19 import android.content.res.Resources;
     20 import android.content.res.TypedArray;
     21 import android.graphics.Bitmap;
     22 import android.graphics.BitmapFactory;
     23 import android.graphics.BitmapShader;
     24 import android.graphics.Canvas;
     25 import android.graphics.ColorFilter;
     26 import android.graphics.Paint;
     27 import android.graphics.PixelFormat;
     28 import android.graphics.Rect;
     29 import android.graphics.Shader;
     30 import android.util.AttributeSet;
     31 import android.util.DisplayMetrics;
     32 import android.view.Gravity;
     33 import org.xmlpull.v1.XmlPullParser;
     34 import org.xmlpull.v1.XmlPullParserException;
     35 
     36 import 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  */
     59 public 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