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 com.android.internal.R;
     20 
     21 import org.xmlpull.v1.XmlPullParser;
     22 import org.xmlpull.v1.XmlPullParserException;
     23 
     24 import android.annotation.NonNull;
     25 import android.annotation.Nullable;
     26 import android.content.res.Resources;
     27 import android.content.res.Resources.Theme;
     28 import android.content.res.TypedArray;
     29 import android.graphics.Canvas;
     30 import android.graphics.PixelFormat;
     31 import android.graphics.Rect;
     32 import android.util.AttributeSet;
     33 import android.util.TypedValue;
     34 import android.view.Gravity;
     35 
     36 import java.io.IOException;
     37 
     38 /**
     39  * A Drawable that changes the size of another Drawable based on its current
     40  * level value. You can control how much the child Drawable changes in width
     41  * and height based on the level, as well as a gravity to control where it is
     42  * placed in its overall container. Most often used to implement things like
     43  * progress bars.
     44  * <p>
     45  * The default level may be specified from XML using the
     46  * {@link android.R.styleable#ScaleDrawable_level android:level} property. When
     47  * this property is not specified, the default level is 0, which corresponds to
     48  * zero height and/or width depending on the values specified for
     49  * {@code android.R.styleable#ScaleDrawable_scaleWidth scaleWidth} and
     50  * {@code android.R.styleable#ScaleDrawable_scaleHeight scaleHeight}. At run
     51  * time, the level may be set via {@link #setLevel(int)}.
     52  * <p>
     53  * A scale drawable may be defined in an XML file with the {@code <scale>}
     54  * element. For more information, see the guide to
     55  * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable
     56  * Resources</a>.
     57  *
     58  * @attr ref android.R.styleable#ScaleDrawable_scaleWidth
     59  * @attr ref android.R.styleable#ScaleDrawable_scaleHeight
     60  * @attr ref android.R.styleable#ScaleDrawable_scaleGravity
     61  * @attr ref android.R.styleable#ScaleDrawable_drawable
     62  * @attr ref android.R.styleable#ScaleDrawable_level
     63  */
     64 public class ScaleDrawable extends DrawableWrapper {
     65     private static final int MAX_LEVEL = 10000;
     66 
     67     private final Rect mTmpRect = new Rect();
     68 
     69     private ScaleState mState;
     70 
     71     ScaleDrawable() {
     72         this(new ScaleState(null, null), null);
     73     }
     74 
     75     /**
     76      * Creates a new scale drawable with the specified gravity and scale
     77      * properties.
     78      *
     79      * @param drawable the drawable to scale
     80      * @param gravity gravity constant (see {@link Gravity} used to position
     81      *                the scaled drawable within the parent container
     82      * @param scaleWidth width scaling factor [0...1] to use then the level is
     83      *                   at the maximum value, or -1 to not scale width
     84      * @param scaleHeight height scaling factor [0...1] to use then the level
     85      *                    is at the maximum value, or -1 to not scale height
     86      */
     87     public ScaleDrawable(Drawable drawable, int gravity, float scaleWidth, float scaleHeight) {
     88         this(new ScaleState(null, null), null);
     89 
     90         mState.mGravity = gravity;
     91         mState.mScaleWidth = scaleWidth;
     92         mState.mScaleHeight = scaleHeight;
     93 
     94         setDrawable(drawable);
     95     }
     96 
     97     @Override
     98     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
     99             @NonNull AttributeSet attrs, @Nullable Theme theme)
    100             throws XmlPullParserException, IOException {
    101         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ScaleDrawable);
    102 
    103         // Inflation will advance the XmlPullParser and AttributeSet.
    104         super.inflate(r, parser, attrs, theme);
    105 
    106         updateStateFromTypedArray(a);
    107         verifyRequiredAttributes(a);
    108         a.recycle();
    109 
    110         updateLocalState();
    111     }
    112 
    113     @Override
    114     public void applyTheme(@NonNull Theme t) {
    115         super.applyTheme(t);
    116 
    117         final ScaleState state = mState;
    118         if (state == null) {
    119             return;
    120         }
    121 
    122         if (state.mThemeAttrs != null) {
    123             final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ScaleDrawable);
    124             try {
    125                 updateStateFromTypedArray(a);
    126                 verifyRequiredAttributes(a);
    127             } catch (XmlPullParserException e) {
    128                 rethrowAsRuntimeException(e);
    129             } finally {
    130                 a.recycle();
    131             }
    132         }
    133 
    134         updateLocalState();
    135     }
    136 
    137     private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
    138         // If we're not waiting on a theme, verify required attributes.
    139         if (getDrawable() == null && (mState.mThemeAttrs == null
    140                 || mState.mThemeAttrs[R.styleable.ScaleDrawable_drawable] == 0)) {
    141             throw new XmlPullParserException(a.getPositionDescription()
    142                     + ": <scale> tag requires a 'drawable' attribute or "
    143                     + "child tag defining a drawable");
    144         }
    145     }
    146 
    147     private void updateStateFromTypedArray(@NonNull TypedArray a) {
    148         final ScaleState state = mState;
    149         if (state == null) {
    150             return;
    151         }
    152 
    153         // Account for any configuration changes.
    154         state.mChangingConfigurations |= a.getChangingConfigurations();
    155 
    156         // Extract the theme attributes, if any.
    157         state.mThemeAttrs = a.extractThemeAttrs();
    158 
    159         state.mScaleWidth = getPercent(a,
    160                 R.styleable.ScaleDrawable_scaleWidth, state.mScaleWidth);
    161         state.mScaleHeight = getPercent(a,
    162                 R.styleable.ScaleDrawable_scaleHeight, state.mScaleHeight);
    163         state.mGravity = a.getInt(
    164                 R.styleable.ScaleDrawable_scaleGravity, state.mGravity);
    165         state.mUseIntrinsicSizeAsMin = a.getBoolean(
    166                 R.styleable.ScaleDrawable_useIntrinsicSizeAsMinimum, state.mUseIntrinsicSizeAsMin);
    167         state.mInitialLevel = a.getInt(
    168                 R.styleable.ScaleDrawable_level, state.mInitialLevel);
    169     }
    170 
    171     private static float getPercent(TypedArray a, int index, float defaultValue) {
    172         final int type = a.getType(index);
    173         if (type == TypedValue.TYPE_FRACTION || type == TypedValue.TYPE_NULL) {
    174             return a.getFraction(index, 1, 1, defaultValue);
    175         }
    176 
    177         // Coerce to float.
    178         final String s = a.getString(index);
    179         if (s != null) {
    180             if (s.endsWith("%")) {
    181                 final String f = s.substring(0, s.length() - 1);
    182                 return Float.parseFloat(f) / 100.0f;
    183             }
    184         }
    185 
    186         return defaultValue;
    187     }
    188 
    189     @Override
    190     public void draw(Canvas canvas) {
    191         final Drawable d = getDrawable();
    192         if (d != null && d.getLevel() != 0) {
    193             d.draw(canvas);
    194         }
    195     }
    196 
    197     @Override
    198     public int getOpacity() {
    199         final Drawable d = getDrawable();
    200         if (d.getLevel() == 0) {
    201             return PixelFormat.TRANSPARENT;
    202         }
    203 
    204         final int opacity = d.getOpacity();
    205         if (opacity == PixelFormat.OPAQUE && d.getLevel() < MAX_LEVEL) {
    206             return PixelFormat.TRANSLUCENT;
    207         }
    208 
    209         return opacity;
    210     }
    211 
    212     @Override
    213     protected boolean onLevelChange(int level) {
    214         super.onLevelChange(level);
    215         onBoundsChange(getBounds());
    216         invalidateSelf();
    217         return true;
    218     }
    219 
    220     @Override
    221     protected void onBoundsChange(Rect bounds) {
    222         final Drawable d = getDrawable();
    223         final Rect r = mTmpRect;
    224         final boolean min = mState.mUseIntrinsicSizeAsMin;
    225         final int level = getLevel();
    226 
    227         int w = bounds.width();
    228         if (mState.mScaleWidth > 0) {
    229             final int iw = min ? d.getIntrinsicWidth() : 0;
    230             w -= (int) ((w - iw) * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL);
    231         }
    232 
    233         int h = bounds.height();
    234         if (mState.mScaleHeight > 0) {
    235             final int ih = min ? d.getIntrinsicHeight() : 0;
    236             h -= (int) ((h - ih) * (MAX_LEVEL - level) * mState.mScaleHeight / MAX_LEVEL);
    237         }
    238 
    239         final int layoutDirection = getLayoutDirection();
    240         Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);
    241 
    242         if (w > 0 && h > 0) {
    243             d.setBounds(r.left, r.top, r.right, r.bottom);
    244         }
    245     }
    246 
    247     @Override
    248     DrawableWrapperState mutateConstantState() {
    249         mState = new ScaleState(mState, null);
    250         return mState;
    251     }
    252 
    253     static final class ScaleState extends DrawableWrapper.DrawableWrapperState {
    254         /** Constant used to disable scaling for a particular dimension. */
    255         private static final float DO_NOT_SCALE = -1.0f;
    256 
    257         private int[] mThemeAttrs;
    258 
    259         float mScaleWidth = DO_NOT_SCALE;
    260         float mScaleHeight = DO_NOT_SCALE;
    261         int mGravity = Gravity.LEFT;
    262         boolean mUseIntrinsicSizeAsMin = false;
    263         int mInitialLevel = 0;
    264 
    265         ScaleState(ScaleState orig, Resources res) {
    266             super(orig, res);
    267 
    268             if (orig != null) {
    269                 mScaleWidth = orig.mScaleWidth;
    270                 mScaleHeight = orig.mScaleHeight;
    271                 mGravity = orig.mGravity;
    272                 mUseIntrinsicSizeAsMin = orig.mUseIntrinsicSizeAsMin;
    273                 mInitialLevel = orig.mInitialLevel;
    274             }
    275         }
    276 
    277         @Override
    278         public Drawable newDrawable(Resources res) {
    279             return new ScaleDrawable(this, res);
    280         }
    281     }
    282 
    283     /**
    284      * Creates a new ScaleDrawable based on the specified constant state.
    285      * <p>
    286      * The resulting drawable is guaranteed to have a new constant state.
    287      *
    288      * @param state constant state from which the drawable inherits
    289      */
    290     private ScaleDrawable(ScaleState state, Resources res) {
    291         super(state, res);
    292 
    293         mState = state;
    294 
    295         updateLocalState();
    296     }
    297 
    298     private void updateLocalState() {
    299         setLevel(mState.mInitialLevel);
    300     }
    301 }
    302 
    303