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><mipmap></code> element. 67 * Each mipmap Drawable is defined in a nested <code><item></code>. For example: 68 * <pre> 69 * <mipmap xmlns:android="http://schemas.android.com/apk/res/android"> 70 * <item android:drawable="@drawable/my_image_8" /> 71 * <item android:drawable="@drawable/my_image_32" /> 72 * <item android:drawable="@drawable/my_image_128" /> 73 * </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