Home | History | Annotate | Download | only in drawable
      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 
     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