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