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