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.Insets;
26import android.graphics.NinePatch;
27import android.graphics.Paint;
28import android.graphics.PixelFormat;
29import android.graphics.Rect;
30import android.graphics.Region;
31import android.util.AttributeSet;
32import android.util.DisplayMetrics;
33import android.util.LayoutDirection;
34import android.util.TypedValue;
35import org.xmlpull.v1.XmlPullParser;
36import org.xmlpull.v1.XmlPullParserException;
37
38import java.io.IOException;
39import 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 */
54public 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