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.Canvas;
     24 import android.graphics.ColorFilter;
     25 import android.graphics.Insets;
     26 import android.graphics.NinePatch;
     27 import android.graphics.Paint;
     28 import android.graphics.PixelFormat;
     29 import android.graphics.Rect;
     30 import android.graphics.Region;
     31 import android.util.AttributeSet;
     32 import android.util.DisplayMetrics;
     33 import android.util.LayoutDirection;
     34 import android.util.TypedValue;
     35 import org.xmlpull.v1.XmlPullParser;
     36 import org.xmlpull.v1.XmlPullParserException;
     37 
     38 import java.io.IOException;
     39 import java.io.InputStream;
     40 
     41 /**
     42  *
     43  * A resizeable bitmap, with stretchable areas that you define. This type of image
     44  * is defined in a .png file with a special format.
     45  *
     46  * <div class="special reference">
     47  * <h3>Developer Guides</h3>
     48  * <p>For more information about how to use a NinePatchDrawable, read the
     49  * <a href="{@docRoot}guide/topics/graphics/2d-graphics.html#nine-patch">
     50  * Canvas and Drawables</a> developer guide. For information about creating a NinePatch image
     51  * file using the draw9patch tool, see the
     52  * <a href="{@docRoot}guide/developing/tools/draw9patch.html">Draw 9-patch</a> tool guide.</p></div>
     53  */
     54 public class NinePatchDrawable extends Drawable {
     55     // dithering helps a lot, and is pretty cheap, so default is true
     56     private static final boolean DEFAULT_DITHER = false;
     57     private NinePatchState mNinePatchState;
     58     private NinePatch mNinePatch;
     59     private Rect mPadding;
     60     private Insets mOpticalInsets = Insets.NONE;
     61     private Paint mPaint;
     62     private boolean mMutated;
     63 
     64     private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
     65 
     66     // These are scaled to match the target density.
     67     private int mBitmapWidth;
     68     private int mBitmapHeight;
     69 
     70     NinePatchDrawable() {
     71     }
     72 
     73     /**
     74      * Create drawable from raw nine-patch data, not dealing with density.
     75      * @deprecated Use {@link #NinePatchDrawable(Resources, Bitmap, byte[], Rect, String)}
     76      * to ensure that the drawable has correctly set its target density.
     77      */
     78     @Deprecated
     79     public NinePatchDrawable(Bitmap bitmap, byte[] chunk, Rect padding, String srcName) {
     80         this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding), null);
     81     }
     82 
     83     /**
     84      * Create drawable from raw nine-patch data, setting initial target density
     85      * based on the display metrics of the resources.
     86      */
     87     public NinePatchDrawable(Resources res, Bitmap bitmap, byte[] chunk,
     88             Rect padding, String srcName) {
     89         this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding), res);
     90         mNinePatchState.mTargetDensity = mTargetDensity;
     91     }
     92 
     93     /**
     94      * Create drawable from raw nine-patch data, setting initial target density
     95      * based on the display metrics of the resources.
     96      *
     97      * @hide
     98      */
     99     public NinePatchDrawable(Resources res, Bitmap bitmap, byte[] chunk,
    100             Rect padding, Rect opticalInsets, String srcName) {
    101         this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding, opticalInsets), res);
    102         mNinePatchState.mTargetDensity = mTargetDensity;
    103     }
    104 
    105     /**
    106      * Create drawable from existing nine-patch, not dealing with density.
    107      * @deprecated Use {@link #NinePatchDrawable(Resources, NinePatch)}
    108      * to ensure that the drawable has correctly set its target density.
    109      */
    110     @Deprecated
    111     public NinePatchDrawable(NinePatch patch) {
    112         this(new NinePatchState(patch, new Rect()), null);
    113     }
    114 
    115     /**
    116      * Create drawable from existing nine-patch, setting initial target density
    117      * based on the display metrics of the resources.
    118      */
    119     public NinePatchDrawable(Resources res, NinePatch patch) {
    120         this(new NinePatchState(patch, new Rect()), res);
    121         mNinePatchState.mTargetDensity = mTargetDensity;
    122     }
    123 
    124     private void setNinePatchState(NinePatchState state, Resources res) {
    125         mNinePatchState = state;
    126         mNinePatch = state.mNinePatch;
    127         mPadding = state.mPadding;
    128         mTargetDensity = res != null ? res.getDisplayMetrics().densityDpi
    129                 : state.mTargetDensity;
    130         //noinspection PointlessBooleanExpression
    131         if (state.mDither != DEFAULT_DITHER) {
    132             // avoid calling the setter unless we need to, since it does a
    133             // lazy allocation of a paint
    134             setDither(state.mDither);
    135         }
    136         setAutoMirrored(state.mAutoMirrored);
    137         if (mNinePatch != null) {
    138             computeBitmapSize();
    139         }
    140     }
    141 
    142     /**
    143      * Set the density scale at which this drawable will be rendered. This
    144      * method assumes the drawable will be rendered at the same density as the
    145      * specified canvas.
    146      *
    147      * @param canvas The Canvas from which the density scale must be obtained.
    148      *
    149      * @see android.graphics.Bitmap#setDensity(int)
    150      * @see android.graphics.Bitmap#getDensity()
    151      */
    152     public void setTargetDensity(Canvas canvas) {
    153         setTargetDensity(canvas.getDensity());
    154     }
    155 
    156     /**
    157      * Set the density scale at which this drawable will be rendered.
    158      *
    159      * @param metrics The DisplayMetrics indicating the density scale for this drawable.
    160      *
    161      * @see android.graphics.Bitmap#setDensity(int)
    162      * @see android.graphics.Bitmap#getDensity()
    163      */
    164     public void setTargetDensity(DisplayMetrics metrics) {
    165         setTargetDensity(metrics.densityDpi);
    166     }
    167 
    168     /**
    169      * Set the density at which this drawable will be rendered.
    170      *
    171      * @param density The density scale for this drawable.
    172      *
    173      * @see android.graphics.Bitmap#setDensity(int)
    174      * @see android.graphics.Bitmap#getDensity()
    175      */
    176     public void setTargetDensity(int density) {
    177         if (density != mTargetDensity) {
    178             mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
    179             if (mNinePatch != null) {
    180                 computeBitmapSize();
    181             }
    182             invalidateSelf();
    183         }
    184     }
    185 
    186     private static Insets scaleFromDensity(Insets insets, int sdensity, int tdensity) {
    187         int left = Bitmap.scaleFromDensity(insets.left, sdensity, tdensity);
    188         int top = Bitmap.scaleFromDensity(insets.top, sdensity, tdensity);
    189         int right = Bitmap.scaleFromDensity(insets.right, sdensity, tdensity);
    190         int bottom = Bitmap.scaleFromDensity(insets.bottom, sdensity, tdensity);
    191         return Insets.of(left, top, right, bottom);
    192     }
    193 
    194     private void computeBitmapSize() {
    195         final int sdensity = mNinePatch.getDensity();
    196         final int tdensity = mTargetDensity;
    197         if (sdensity == tdensity) {
    198             mBitmapWidth = mNinePatch.getWidth();
    199             mBitmapHeight = mNinePatch.getHeight();
    200             mOpticalInsets = mNinePatchState.mOpticalInsets;
    201         } else {
    202             mBitmapWidth = Bitmap.scaleFromDensity(mNinePatch.getWidth(), sdensity, tdensity);
    203             mBitmapHeight = Bitmap.scaleFromDensity(mNinePatch.getHeight(), sdensity, tdensity);
    204             if (mNinePatchState.mPadding != null && mPadding != null) {
    205                 Rect dest = mPadding;
    206                 Rect src = mNinePatchState.mPadding;
    207                 if (dest == src) {
    208                     mPadding = dest = new Rect(src);
    209                 }
    210                 dest.left = Bitmap.scaleFromDensity(src.left, sdensity, tdensity);
    211                 dest.top = Bitmap.scaleFromDensity(src.top, sdensity, tdensity);
    212                 dest.right = Bitmap.scaleFromDensity(src.right, sdensity, tdensity);
    213                 dest.bottom = Bitmap.scaleFromDensity(src.bottom, sdensity, tdensity);
    214             }
    215             mOpticalInsets = scaleFromDensity(mNinePatchState.mOpticalInsets, sdensity, tdensity);
    216         }
    217     }
    218 
    219     @Override
    220     public void draw(Canvas canvas) {
    221         final Rect bounds = getBounds();
    222         final boolean needsMirroring = needsMirroring();
    223         if (needsMirroring) {
    224             canvas.save();
    225             // Mirror the 9patch
    226             canvas.translate(bounds.right - bounds.left, 0);
    227             canvas.scale(-1.0f, 1.0f);
    228         }
    229         mNinePatch.draw(canvas, bounds, mPaint);
    230         if (needsMirroring) {
    231             canvas.restore();
    232         }
    233     }
    234 
    235     @Override
    236     public int getChangingConfigurations() {
    237         return super.getChangingConfigurations() | mNinePatchState.mChangingConfigurations;
    238     }
    239 
    240     @Override
    241     public boolean getPadding(Rect padding) {
    242         if (needsMirroring()) {
    243             padding.set(mPadding.right, mPadding.top, mPadding.left, mPadding.bottom);
    244         } else {
    245             padding.set(mPadding);
    246         }
    247         return (padding.left | padding.top | padding.right | padding.bottom) != 0;
    248     }
    249 
    250     /**
    251      * @hide
    252      */
    253     @Override
    254     public Insets getOpticalInsets() {
    255         if (needsMirroring()) {
    256             return Insets.of(mOpticalInsets.right, mOpticalInsets.top, mOpticalInsets.right,
    257                     mOpticalInsets.bottom);
    258         } else {
    259             return mOpticalInsets;
    260         }
    261     }
    262 
    263     @Override
    264     public void setAlpha(int alpha) {
    265         if (mPaint == null && alpha == 0xFF) {
    266             // Fast common case -- leave at normal alpha.
    267             return;
    268         }
    269         getPaint().setAlpha(alpha);
    270         invalidateSelf();
    271     }
    272 
    273     @Override
    274     public int getAlpha() {
    275         if (mPaint == null) {
    276             // Fast common case -- normal alpha.
    277             return 0xFF;
    278         }
    279         return getPaint().getAlpha();
    280     }
    281 
    282     @Override
    283     public void setColorFilter(ColorFilter cf) {
    284         if (mPaint == null && cf == null) {
    285             // Fast common case -- leave at no color filter.
    286             return;
    287         }
    288         getPaint().setColorFilter(cf);
    289         invalidateSelf();
    290     }
    291 
    292     @Override
    293     public void setDither(boolean dither) {
    294         //noinspection PointlessBooleanExpression
    295         if (mPaint == null && dither == DEFAULT_DITHER) {
    296             // Fast common case -- leave at default dither.
    297             return;
    298         }
    299         getPaint().setDither(dither);
    300         invalidateSelf();
    301     }
    302 
    303     @Override
    304     public void setAutoMirrored(boolean mirrored) {
    305         mNinePatchState.mAutoMirrored = mirrored;
    306     }
    307 
    308     private boolean needsMirroring() {
    309         return isAutoMirrored() && getLayoutDirection() == LayoutDirection.RTL;
    310     }
    311 
    312     @Override
    313     public boolean isAutoMirrored() {
    314         return mNinePatchState.mAutoMirrored;
    315     }
    316 
    317     @Override
    318     public void setFilterBitmap(boolean filter) {
    319         getPaint().setFilterBitmap(filter);
    320         invalidateSelf();
    321     }
    322 
    323     @Override
    324     public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
    325             throws XmlPullParserException, IOException {
    326         super.inflate(r, parser, attrs);
    327 
    328         TypedArray a = r.obtainAttributes(attrs, com.android.internal.R.styleable.NinePatchDrawable);
    329 
    330         final int id = a.getResourceId(com.android.internal.R.styleable.NinePatchDrawable_src, 0);
    331         if (id == 0) {
    332             throw new XmlPullParserException(parser.getPositionDescription() +
    333                     ": <nine-patch> requires a valid src attribute");
    334         }
    335 
    336         final boolean dither = a.getBoolean(
    337                 com.android.internal.R.styleable.NinePatchDrawable_dither, DEFAULT_DITHER);
    338         final BitmapFactory.Options options = new BitmapFactory.Options();
    339         if (dither) {
    340             options.inDither = false;
    341         }
    342         options.inScreenDensity = r.getDisplayMetrics().noncompatDensityDpi;
    343 
    344         final Rect padding = new Rect();
    345         final Rect opticalInsets = new Rect();
    346         Bitmap bitmap = null;
    347 
    348         try {
    349             final TypedValue value = new TypedValue();
    350             final InputStream is = r.openRawResource(id, value);
    351 
    352             bitmap = BitmapFactory.decodeResourceStream(r, value, is, padding, options);
    353 
    354             is.close();
    355         } catch (IOException e) {
    356             // Ignore
    357         }
    358 
    359         if (bitmap == null) {
    360             throw new XmlPullParserException(parser.getPositionDescription() +
    361                     ": <nine-patch> requires a valid src attribute");
    362         } else if (bitmap.getNinePatchChunk() == null) {
    363             throw new XmlPullParserException(parser.getPositionDescription() +
    364                     ": <nine-patch> requires a valid 9-patch source image");
    365         }
    366 
    367         final boolean automirrored = a.getBoolean(
    368                 com.android.internal.R.styleable.NinePatchDrawable_autoMirrored, false);
    369 
    370         setNinePatchState(new NinePatchState(new NinePatch(bitmap, bitmap.getNinePatchChunk()),
    371                 padding, opticalInsets, dither, automirrored), r);
    372         mNinePatchState.mTargetDensity = mTargetDensity;
    373 
    374         a.recycle();
    375     }
    376 
    377     public Paint getPaint() {
    378         if (mPaint == null) {
    379             mPaint = new Paint();
    380             mPaint.setDither(DEFAULT_DITHER);
    381         }
    382         return mPaint;
    383     }
    384 
    385     /**
    386      * Retrieves the width of the source .png file (before resizing).
    387      */
    388     @Override
    389     public int getIntrinsicWidth() {
    390         return mBitmapWidth;
    391     }
    392 
    393     /**
    394      * Retrieves the height of the source .png file (before resizing).
    395      */
    396     @Override
    397     public int getIntrinsicHeight() {
    398         return mBitmapHeight;
    399     }
    400 
    401     @Override
    402     public int getMinimumWidth() {
    403         return mBitmapWidth;
    404     }
    405 
    406     @Override
    407     public int getMinimumHeight() {
    408         return mBitmapHeight;
    409     }
    410 
    411     /**
    412      * Returns a {@link android.graphics.PixelFormat graphics.PixelFormat}
    413      * value of OPAQUE or TRANSLUCENT.
    414      */
    415     @Override
    416     public int getOpacity() {
    417         return mNinePatch.hasAlpha() || (mPaint != null && mPaint.getAlpha() < 255) ?
    418                 PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
    419     }
    420 
    421     @Override
    422     public Region getTransparentRegion() {
    423         return mNinePatch.getTransparentRegion(getBounds());
    424     }
    425 
    426     @Override
    427     public ConstantState getConstantState() {
    428         mNinePatchState.mChangingConfigurations = getChangingConfigurations();
    429         return mNinePatchState;
    430     }
    431 
    432     @Override
    433     public Drawable mutate() {
    434         if (!mMutated && super.mutate() == this) {
    435             mNinePatchState = new NinePatchState(mNinePatchState);
    436             mNinePatch = mNinePatchState.mNinePatch;
    437             mMutated = true;
    438         }
    439         return this;
    440     }
    441 
    442     final static class NinePatchState extends ConstantState {
    443         final NinePatch mNinePatch;
    444         final Rect mPadding;
    445         final Insets mOpticalInsets;
    446         final boolean mDither;
    447         int mChangingConfigurations;
    448         int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
    449         boolean mAutoMirrored;
    450 
    451         NinePatchState(NinePatch ninePatch, Rect padding) {
    452             this(ninePatch, padding, new Rect(), DEFAULT_DITHER, false);
    453         }
    454 
    455         NinePatchState(NinePatch ninePatch, Rect padding, Rect opticalInsets) {
    456             this(ninePatch, padding, opticalInsets, DEFAULT_DITHER, false);
    457         }
    458 
    459         NinePatchState(NinePatch ninePatch, Rect rect, Rect opticalInsets, boolean dither,
    460                        boolean autoMirror) {
    461             mNinePatch = ninePatch;
    462             mPadding = rect;
    463             mOpticalInsets = Insets.of(opticalInsets);
    464             mDither = dither;
    465             mAutoMirrored = autoMirror;
    466         }
    467 
    468         // Copy constructor
    469 
    470         NinePatchState(NinePatchState state) {
    471             // Note we don't copy the nine patch because it is immutable.
    472             mNinePatch = state.mNinePatch;
    473             // Note we don't copy the padding because it is immutable.
    474             mPadding = state.mPadding;
    475             mOpticalInsets = state.mOpticalInsets;
    476             mDither = state.mDither;
    477             mChangingConfigurations = state.mChangingConfigurations;
    478             mTargetDensity = state.mTargetDensity;
    479             mAutoMirrored = state.mAutoMirrored;
    480         }
    481 
    482         @Override
    483         public Bitmap getBitmap() {
    484             return mNinePatch.getBitmap();
    485         }
    486 
    487         @Override
    488         public Drawable newDrawable() {
    489             return new NinePatchDrawable(this, null);
    490         }
    491 
    492         @Override
    493         public Drawable newDrawable(Resources res) {
    494             return new NinePatchDrawable(this, res);
    495         }
    496 
    497         @Override
    498         public int getChangingConfigurations() {
    499             return mChangingConfigurations;
    500         }
    501     }
    502 
    503     private NinePatchDrawable(NinePatchState state, Resources res) {
    504         setNinePatchState(state, res);
    505     }
    506 }
    507