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