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 org.xmlpull.v1.XmlPullParser;
20import org.xmlpull.v1.XmlPullParserException;
21
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.graphics.Rect;
25import android.util.AttributeSet;
26
27import java.io.IOException;
28
29/**
30 * @hide -- we are probably moving to do MipMaps in another way (more integrated
31 * with the resource system).
32 *
33 * A resource that manages a number of alternate Drawables, and which actually draws the one which
34 * size matches the most closely the drawing bounds. Providing several pre-scaled version of the
35 * drawable helps minimizing the aliasing artifacts that can be introduced by the scaling.
36 *
37 * <p>
38 * Use {@link #addDrawable(Drawable)} to define the different Drawables that will represent the
39 * mipmap levels of this MipmapDrawable. The mipmap Drawable that will actually be used when this
40 * MipmapDrawable is drawn is the one which has the smallest intrinsic height greater or equal than
41 * the bounds' height. This selection ensures that the best available mipmap level is scaled down to
42 * draw this MipmapDrawable.
43 * </p>
44 *
45 * If the bounds' height is larger than the largest mipmap, the largest mipmap will be scaled up.
46 * Note that Drawables without intrinsic height (i.e. with a negative value, such as Color) will
47 * only be used if no other mipmap Drawable are provided. The Drawables' intrinsic heights should
48 * not be changed after the Drawable has been added to this MipmapDrawable.
49 *
50 * <p>
51 * The different mipmaps' parameters (opacity, padding, color filter, gravity...) should typically
52 * be similar to ensure a continuous visual appearance when the MipmapDrawable is scaled. The aspect
53 * ratio of the different mipmaps should especially be equal.
54 * </p>
55 *
56 * A typical example use of a MipmapDrawable would be for an image which is intended to be scaled at
57 * various sizes, and for which one wants to provide pre-scaled versions to precisely control its
58 * appearance.
59 *
60 * <p>
61 * The intrinsic size of a MipmapDrawable are inferred from those of the largest mipmap (in terms of
62 * {@link Drawable#getIntrinsicHeight()}). On the opposite, its minimum
63 * size is defined by the smallest provided mipmap.
64 * </p>
65
66 * It can be defined in an XML file with the <code>&lt;mipmap></code> element.
67 * Each mipmap Drawable is defined in a nested <code>&lt;item></code>. For example:
68 * <pre>
69 * &lt;mipmap xmlns:android="http://schemas.android.com/apk/res/android">
70 *  &lt;item android:drawable="@drawable/my_image_8" />
71 *  &lt;item android:drawable="@drawable/my_image_32" />
72 *  &lt;item android:drawable="@drawable/my_image_128" />
73 * &lt;/mipmap>
74 *</pre>
75 * <p>
76 * With this XML saved into the res/drawable/ folder of the project, it can be referenced as
77 * the drawable for an {@link android.widget.ImageView}. Assuming that the heights of the provided
78 * drawables are respectively 8, 32 and 128 pixels, the first one will be scaled down when the
79 * bounds' height is lower or equal than 8 pixels. The second drawable will then be used up to a
80 * height of 32 pixels and the largest drawable will be used for greater heights.
81 * </p>
82 * @attr ref android.R.styleable#MipmapDrawableItem_drawable
83 */
84public class MipmapDrawable extends DrawableContainer {
85    private final MipmapContainerState mMipmapContainerState;
86    private boolean mMutated;
87
88    public MipmapDrawable() {
89        this(null, null);
90    }
91
92    /**
93     * Adds a Drawable to the list of available mipmap Drawables. The Drawable actually used when
94     * this MipmapDrawable is drawn is determined from its bounds.
95     *
96     * This method has no effect if drawable is null.
97     *
98     * @param drawable The Drawable that will be added to list of available mipmap Drawables.
99     */
100
101    public void addDrawable(Drawable drawable) {
102        if (drawable != null) {
103            mMipmapContainerState.addDrawable(drawable);
104            onDrawableAdded();
105        }
106    }
107
108    private void onDrawableAdded() {
109        // selectDrawable assumes that the container content does not change.
110        // When a Drawable is added, the same index can correspond to a new Drawable, and since
111        // selectDrawable has a fast exit case when oldIndex==newIndex, the new drawable could end
112        // up not being used in place of the previous one if they happen to share the same index.
113        // This make sure the new computed index can actually replace the previous one.
114        selectDrawable(-1);
115        onBoundsChange(getBounds());
116    }
117
118    // overrides from Drawable
119
120    @Override
121    protected void onBoundsChange(Rect bounds) {
122        final int index = mMipmapContainerState.indexForBounds(bounds);
123
124        // Will call invalidateSelf() if needed
125        selectDrawable(index);
126
127        super.onBoundsChange(bounds);
128    }
129
130    @Override
131    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs)
132    throws XmlPullParserException, IOException {
133
134        super.inflate(r, parser, attrs);
135
136        int type;
137
138        final int innerDepth = parser.getDepth() + 1;
139        int depth;
140        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
141                && ((depth = parser.getDepth()) >= innerDepth
142                        || type != XmlPullParser.END_TAG)) {
143            if (type != XmlPullParser.START_TAG) {
144                continue;
145            }
146
147            if (depth > innerDepth || !parser.getName().equals("item")) {
148                continue;
149            }
150
151            TypedArray a = r.obtainAttributes(attrs,
152                    com.android.internal.R.styleable.MipmapDrawableItem);
153
154            int drawableRes = a.getResourceId(
155                    com.android.internal.R.styleable.MipmapDrawableItem_drawable, 0);
156
157            a.recycle();
158
159            Drawable dr;
160            if (drawableRes != 0) {
161                dr = r.getDrawable(drawableRes);
162            } else {
163                while ((type = parser.next()) == XmlPullParser.TEXT) {
164                }
165                if (type != XmlPullParser.START_TAG) {
166                    throw new XmlPullParserException(
167                            parser.getPositionDescription()
168                            + ": <item> tag requires a 'drawable' attribute or "
169                            + "child tag defining a drawable");
170                }
171                dr = Drawable.createFromXmlInner(r, parser, attrs);
172            }
173
174            mMipmapContainerState.addDrawable(dr);
175        }
176
177        onDrawableAdded();
178    }
179
180    @Override
181    public Drawable mutate() {
182        if (!mMutated && super.mutate() == this) {
183            mMipmapContainerState.mMipmapHeights = mMipmapContainerState.mMipmapHeights.clone();
184            mMutated = true;
185        }
186        return this;
187    }
188
189    private final static class MipmapContainerState extends DrawableContainerState {
190        private int[] mMipmapHeights;
191
192        MipmapContainerState(MipmapContainerState orig, MipmapDrawable owner, Resources res) {
193            super(orig, owner, res);
194
195            if (orig != null) {
196                mMipmapHeights = orig.mMipmapHeights;
197            } else {
198                mMipmapHeights = new int[getChildren().length];
199            }
200
201            // Change the default value
202            setConstantSize(true);
203        }
204
205        /**
206         * Returns the index of the child mipmap drawable that will best fit the provided bounds.
207         * This index is determined by comparing bounds' height and children intrinsic heights.
208         * The returned mipmap index is the smallest mipmap which height is greater or equal than
209         * the bounds' height. If the bounds' height is larger than the largest mipmap, the largest
210         * mipmap index is returned.
211         *
212         * @param bounds The bounds of the MipMapDrawable.
213         * @return The index of the child Drawable that will best fit these bounds, or -1 if there
214         * are no children mipmaps.
215         */
216        public int indexForBounds(Rect bounds) {
217            final int boundsHeight = bounds.height();
218            final int N = getChildCount();
219            for (int i = 0; i < N; i++) {
220                if (boundsHeight <= mMipmapHeights[i]) {
221                    return i;
222                }
223            }
224
225            // No mipmap larger than bounds found. Use largest one which will be scaled up.
226            if (N > 0) {
227                return N - 1;
228            }
229            // No Drawable mipmap at all
230            return -1;
231        }
232
233        /**
234         * Adds a Drawable to the list of available mipmap Drawables. This list can be retrieved
235         * using {@link DrawableContainer.DrawableContainerState#getChildren()} and this method
236         * ensures that it is always sorted by increasing {@link Drawable#getIntrinsicHeight()}.
237         *
238         * @param drawable The Drawable that will be added to children list
239         */
240        public void addDrawable(Drawable drawable) {
241            // Insert drawable in last position, correctly resetting cached values and
242            // especially mComputedConstantSize
243            int pos = addChild(drawable);
244
245            // Bubble sort the last drawable to restore the sort by intrinsic height
246            final int drawableHeight = drawable.getIntrinsicHeight();
247
248            while (pos > 0) {
249                final Drawable previousDrawable = mDrawables[pos-1];
250                final int previousIntrinsicHeight = previousDrawable.getIntrinsicHeight();
251
252                if (drawableHeight < previousIntrinsicHeight) {
253                    mDrawables[pos] = previousDrawable;
254                    mMipmapHeights[pos] = previousIntrinsicHeight;
255
256                    mDrawables[pos-1] = drawable;
257                    mMipmapHeights[pos-1] = drawableHeight;
258                    pos--;
259                } else {
260                    break;
261                }
262            }
263        }
264
265        /**
266         * Intrinsic sizes are those of the largest available mipmap.
267         * Minimum sizes are those of the smallest available mipmap.
268         */
269        @Override
270        protected void computeConstantSize() {
271            final int N = getChildCount();
272            if (N > 0) {
273                final Drawable smallestDrawable = mDrawables[0];
274                mConstantMinimumWidth = smallestDrawable.getMinimumWidth();
275                mConstantMinimumHeight = smallestDrawable.getMinimumHeight();
276
277                final Drawable largestDrawable = mDrawables[N-1];
278                mConstantWidth = largestDrawable.getIntrinsicWidth();
279                mConstantHeight = largestDrawable.getIntrinsicHeight();
280            } else {
281                mConstantWidth = mConstantHeight = -1;
282                mConstantMinimumWidth = mConstantMinimumHeight = 0;
283            }
284            mComputedConstantSize = true;
285        }
286
287        @Override
288        public Drawable newDrawable() {
289            return new MipmapDrawable(this, null);
290        }
291
292        @Override
293        public Drawable newDrawable(Resources res) {
294            return new MipmapDrawable(this, res);
295        }
296
297        @Override
298        public void growArray(int oldSize, int newSize) {
299            super.growArray(oldSize, newSize);
300            int[] newInts = new int[newSize];
301            System.arraycopy(mMipmapHeights, 0, newInts, 0, oldSize);
302            mMipmapHeights = newInts;
303        }
304    }
305
306    private MipmapDrawable(MipmapContainerState state, Resources res) {
307        MipmapContainerState as = new MipmapContainerState(state, this, res);
308        mMipmapContainerState = as;
309        setConstantState(as);
310        onDrawableAdded();
311    }
312}
313