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