• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.graphics.drawable;
18 
19 import org.xmlpull.v1.XmlPullParser;
20 import org.xmlpull.v1.XmlPullParserException;
21 
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.graphics.Rect;
25 import android.util.AttributeSet;
26 
27 import 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  */
84 public class MipmapDrawable extends DrawableContainer {
85     private final MipmapContainerState mMipmapContainerState;
86     private boolean mMutated;
87 
MipmapDrawable()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 
addDrawable(Drawable drawable)101     public void addDrawable(Drawable drawable) {
102         if (drawable != null) {
103             mMipmapContainerState.addDrawable(drawable);
104             onDrawableAdded();
105         }
106     }
107 
onDrawableAdded()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
onBoundsChange(Rect bounds)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
inflate(Resources r, XmlPullParser parser, AttributeSet attrs)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
mutate()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 
MipmapContainerState(MipmapContainerState orig, MipmapDrawable owner, Resources res)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          */
indexForBounds(Rect bounds)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          */
addDrawable(Drawable drawable)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
computeConstantSize()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
newDrawable()288         public Drawable newDrawable() {
289             return new MipmapDrawable(this, null);
290         }
291 
292         @Override
newDrawable(Resources res)293         public Drawable newDrawable(Resources res) {
294             return new MipmapDrawable(this, res);
295         }
296 
297         @Override
growArray(int oldSize, int newSize)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 
MipmapDrawable(MipmapContainerState state, Resources res)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