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