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