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