• 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 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 
ScaleDrawable()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      */
ScaleDrawable(Drawable drawable, int gravity, float scaleWidth, float scaleHeight)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
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)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
applyTheme(@onNull Theme t)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 
verifyRequiredAttributes(@onNull TypedArray a)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 
updateStateFromTypedArray(@onNull TypedArray a)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 
getPercent(TypedArray a, int index, float defaultValue)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
draw(Canvas canvas)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
getOpacity()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
onLevelChange(int level)213     protected boolean onLevelChange(int level) {
214         super.onLevelChange(level);
215         onBoundsChange(getBounds());
216         invalidateSelf();
217         return true;
218     }
219 
220     @Override
onBoundsChange(Rect bounds)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
mutateConstantState()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 
ScaleState(ScaleState orig, Resources res)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
newDrawable(Resources res)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      */
ScaleDrawable(ScaleState state, Resources res)290     private ScaleDrawable(ScaleState state, Resources res) {
291         super(state, res);
292 
293         mState = state;
294 
295         updateLocalState();
296     }
297 
updateLocalState()298     private void updateLocalState() {
299         setLevel(mState.mInitialLevel);
300     }
301 }
302 
303