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.graphics.*;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.util.AttributeSet;
23import android.util.DisplayMetrics;
24import android.util.Log;
25import android.util.TypedValue;
26import org.xmlpull.v1.XmlPullParser;
27import org.xmlpull.v1.XmlPullParserException;
28
29import java.io.IOException;
30import java.io.InputStream;
31
32/**
33 *
34 * A resizeable bitmap, with stretchable areas that you define. This type of image
35 * is defined in a .png file with a special format, described in <a link="../../../resources.html#ninepatch">
36 * Resources</a>.
37 *
38 */
39public class NinePatchDrawable extends Drawable {
40    // dithering helps a lot, and is pretty cheap, so default is true
41    private static final boolean DEFAULT_DITHER = true;
42    private NinePatchState mNinePatchState;
43    private NinePatch mNinePatch;
44    private Rect mPadding;
45    private Paint mPaint;
46    private boolean mMutated;
47
48    private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
49
50    // These are scaled to match the target density.
51    private int mBitmapWidth;
52    private int mBitmapHeight;
53
54    NinePatchDrawable() {
55    }
56
57    /**
58     * Create drawable from raw nine-patch data, not dealing with density.
59     * @deprecated Use {@link #NinePatchDrawable(Resources, Bitmap, byte[], Rect, String)}
60     * to ensure that the drawable has correctly set its target density.
61     */
62    @Deprecated
63    public NinePatchDrawable(Bitmap bitmap, byte[] chunk, Rect padding, String srcName) {
64        this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding), null);
65    }
66
67    /**
68     * Create drawable from raw nine-patch data, setting initial target density
69     * based on the display metrics of the resources.
70     */
71    public NinePatchDrawable(Resources res, Bitmap bitmap, byte[] chunk,
72            Rect padding, String srcName) {
73        this(new NinePatchState(new NinePatch(bitmap, chunk, srcName), padding), res);
74        mNinePatchState.mTargetDensity = mTargetDensity;
75    }
76
77    /**
78     * Create drawable from existing nine-patch, not dealing with density.
79     * @deprecated Use {@link #NinePatchDrawable(Resources, NinePatch)}
80     * to ensure that the drawable has correctly set its target density.
81     */
82    @Deprecated
83    public NinePatchDrawable(NinePatch patch) {
84        this(new NinePatchState(patch, new Rect()), null);
85    }
86
87    /**
88     * Create drawable from existing nine-patch, setting initial target density
89     * based on the display metrics of the resources.
90     */
91    public NinePatchDrawable(Resources res, NinePatch patch) {
92        this(new NinePatchState(patch, new Rect()), res);
93        mNinePatchState.mTargetDensity = mTargetDensity;
94    }
95
96    private void setNinePatchState(NinePatchState state, Resources res) {
97        mNinePatchState = state;
98        mNinePatch = state.mNinePatch;
99        mPadding = state.mPadding;
100        mTargetDensity = res != null ? res.getDisplayMetrics().densityDpi
101                : state.mTargetDensity;
102        if (DEFAULT_DITHER != state.mDither) {
103            // avoid calling the setter unless we need to, since it does a
104            // lazy allocation of a paint
105            setDither(state.mDither);
106        }
107        if (mNinePatch != null) {
108            computeBitmapSize();
109        }
110    }
111
112    /**
113     * Set the density scale at which this drawable will be rendered. This
114     * method assumes the drawable will be rendered at the same density as the
115     * specified canvas.
116     *
117     * @param canvas The Canvas from which the density scale must be obtained.
118     *
119     * @see android.graphics.Bitmap#setDensity(int)
120     * @see android.graphics.Bitmap#getDensity()
121     */
122    public void setTargetDensity(Canvas canvas) {
123        setTargetDensity(canvas.getDensity());
124    }
125
126    /**
127     * Set the density scale at which this drawable will be rendered.
128     *
129     * @param metrics The DisplayMetrics indicating the density scale for this drawable.
130     *
131     * @see android.graphics.Bitmap#setDensity(int)
132     * @see android.graphics.Bitmap#getDensity()
133     */
134    public void setTargetDensity(DisplayMetrics metrics) {
135        mTargetDensity = metrics.densityDpi;
136        if (mNinePatch != null) {
137            computeBitmapSize();
138        }
139    }
140
141    /**
142     * Set the density at which this drawable will be rendered.
143     *
144     * @param density The density scale for this drawable.
145     *
146     * @see android.graphics.Bitmap#setDensity(int)
147     * @see android.graphics.Bitmap#getDensity()
148     */
149    public void setTargetDensity(int density) {
150        mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
151        if (mNinePatch != null) {
152            computeBitmapSize();
153        }
154    }
155
156    private void computeBitmapSize() {
157        final int sdensity = mNinePatch.getDensity();
158        final int tdensity = mTargetDensity;
159        if (sdensity == tdensity) {
160            mBitmapWidth = mNinePatch.getWidth();
161            mBitmapHeight = mNinePatch.getHeight();
162        } else {
163            mBitmapWidth = Bitmap.scaleFromDensity(mNinePatch.getWidth(),
164                    sdensity, tdensity);
165            mBitmapHeight = Bitmap.scaleFromDensity(mNinePatch.getHeight(),
166                    sdensity, tdensity);
167            if (mNinePatchState.mPadding != null && mPadding != null) {
168                Rect dest = mPadding;
169                Rect src = mNinePatchState.mPadding;
170                if (dest == src) {
171                    mPadding = dest = new Rect(src);
172                }
173                dest.left = Bitmap.scaleFromDensity(src.left, sdensity, tdensity);
174                dest.top = Bitmap.scaleFromDensity(src.top, sdensity, tdensity);
175                dest.right = Bitmap.scaleFromDensity(src.right, sdensity, tdensity);
176                dest.bottom = Bitmap.scaleFromDensity(src.bottom, sdensity, tdensity);
177            }
178        }
179    }
180
181    // overrides
182
183    @Override
184    public void draw(Canvas canvas) {
185        if (false) {
186            float[] pts = new float[2];
187            canvas.getMatrix().mapPoints(pts);
188            Log.v("9patch", "Drawing 9-patch @ " + pts[0] + "," + pts[1] + ": " + getBounds());
189        }
190        mNinePatch.draw(canvas, getBounds(), mPaint);
191    }
192
193    @Override
194    public int getChangingConfigurations() {
195        return super.getChangingConfigurations() | mNinePatchState.mChangingConfigurations;
196    }
197
198    @Override
199    public boolean getPadding(Rect padding) {
200        padding.set(mPadding);
201        return true;
202    }
203
204    @Override
205    public void setAlpha(int alpha) {
206        getPaint().setAlpha(alpha);
207    }
208
209    @Override
210    public void setColorFilter(ColorFilter cf) {
211        getPaint().setColorFilter(cf);
212    }
213
214    @Override
215    public void setDither(boolean dither) {
216        getPaint().setDither(dither);
217    }
218
219    @Override
220    public void setFilterBitmap(boolean filter) {
221        // at the moment, we see no quality improvement, but a big slowdown
222        // with filtering, so ignore this call for now
223        //
224        //getPaint().setFilterBitmap(filter);
225    }
226
227    @Override
228    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
229            throws XmlPullParserException, IOException {
230        super.inflate(r, parser, attrs);
231
232        TypedArray a = r.obtainAttributes(attrs, com.android.internal.R.styleable.NinePatchDrawable);
233
234        final int id = a.getResourceId(com.android.internal.R.styleable.NinePatchDrawable_src, 0);
235        if (id == 0) {
236            throw new XmlPullParserException(parser.getPositionDescription() +
237                    ": <nine-patch> requires a valid src attribute");
238        }
239
240        final boolean dither = a.getBoolean(
241                com.android.internal.R.styleable.NinePatchDrawable_dither,
242                DEFAULT_DITHER);
243        final BitmapFactory.Options options = new BitmapFactory.Options();
244        if (dither) {
245            options.inDither = false;
246        }
247        options.inScreenDensity = DisplayMetrics.DENSITY_DEVICE;
248
249        final Rect padding = new Rect();
250        Bitmap bitmap = null;
251
252        try {
253            final TypedValue value = new TypedValue();
254            final InputStream is = r.openRawResource(id, value);
255
256            bitmap = BitmapFactory.decodeResourceStream(r, value, is, padding, options);
257
258            is.close();
259        } catch (IOException e) {
260            // Ignore
261        }
262
263        if (bitmap == null) {
264            throw new XmlPullParserException(parser.getPositionDescription() +
265                    ": <nine-patch> requires a valid src attribute");
266        } else if (bitmap.getNinePatchChunk() == null) {
267            throw new XmlPullParserException(parser.getPositionDescription() +
268                    ": <nine-patch> requires a valid 9-patch source image");
269        }
270
271        setNinePatchState(new NinePatchState(
272                new NinePatch(bitmap, bitmap.getNinePatchChunk(), "XML 9-patch"),
273                padding, dither), r);
274        mNinePatchState.mTargetDensity = mTargetDensity;
275
276        a.recycle();
277    }
278
279    public Paint getPaint() {
280        if (mPaint == null) {
281            mPaint = new Paint();
282            mPaint.setDither(DEFAULT_DITHER);
283        }
284        return mPaint;
285    }
286
287    /**
288     * Retrieves the width of the source .png file (before resizing).
289     */
290    @Override
291    public int getIntrinsicWidth() {
292        return mBitmapWidth;
293    }
294
295    /**
296     * Retrieves the height of the source .png file (before resizing).
297     */
298    @Override
299    public int getIntrinsicHeight() {
300        return mBitmapHeight;
301    }
302
303    @Override
304    public int getMinimumWidth() {
305        return mBitmapWidth;
306    }
307
308    @Override
309    public int getMinimumHeight() {
310        return mBitmapHeight;
311    }
312
313    /**
314     * Returns a {@link android.graphics.PixelFormat graphics.PixelFormat}
315     * value of OPAQUE or TRANSLUCENT.
316     */
317    @Override
318    public int getOpacity() {
319        return mNinePatch.hasAlpha() || (mPaint != null && mPaint.getAlpha() < 255) ?
320                PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
321    }
322
323    @Override
324    public Region getTransparentRegion() {
325        return mNinePatch.getTransparentRegion(getBounds());
326    }
327
328    @Override
329    public ConstantState getConstantState() {
330        mNinePatchState.mChangingConfigurations = super.getChangingConfigurations();
331        return mNinePatchState;
332    }
333
334    @Override
335    public Drawable mutate() {
336        if (!mMutated && super.mutate() == this) {
337            mNinePatchState = new NinePatchState(mNinePatchState);
338            mNinePatch = mNinePatchState.mNinePatch;
339            mMutated = true;
340        }
341        return this;
342    }
343
344    final static class NinePatchState extends ConstantState {
345        final NinePatch mNinePatch;
346        final Rect mPadding;
347        final boolean mDither;
348        int mChangingConfigurations;
349        int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
350
351        NinePatchState(NinePatch ninePatch, Rect padding) {
352            this(ninePatch, padding, DEFAULT_DITHER);
353        }
354
355        NinePatchState(NinePatch ninePatch, Rect rect, boolean dither) {
356            mNinePatch = ninePatch;
357            mPadding = rect;
358            mDither = dither;
359        }
360
361        NinePatchState(NinePatchState state) {
362            mNinePatch = new NinePatch(state.mNinePatch);
363            // Note we don't copy the padding because it is immutable.
364            mPadding = state.mPadding;
365            mDither = state.mDither;
366            mChangingConfigurations = state.mChangingConfigurations;
367            mTargetDensity = state.mTargetDensity;
368        }
369
370        @Override
371        public Drawable newDrawable() {
372            return new NinePatchDrawable(this, null);
373        }
374
375        @Override
376        public Drawable newDrawable(Resources res) {
377            return new NinePatchDrawable(this, res);
378        }
379
380        @Override
381        public int getChangingConfigurations() {
382            return mChangingConfigurations;
383        }
384    }
385
386    private NinePatchDrawable(NinePatchState state, Resources res) {
387        setNinePatchState(state, res);
388    }
389}
390