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.content.res.ColorStateList;
25 import android.content.res.Resources;
26 import android.content.res.TypedArray;
27 import android.content.res.Resources.Theme;
28 import android.graphics.*;
29 import android.graphics.PorterDuff.Mode;
30 import android.view.Gravity;
31 import android.util.AttributeSet;
32 
33 import java.io.IOException;
34 
35 /**
36  * A Drawable that clips another Drawable based on this Drawable's current
37  * level value.  You can control how much the child Drawable gets clipped in width
38  * and height based on the level, as well as a gravity to control where it is
39  * placed in its overall container.  Most often used to implement things like
40  * progress bars, by increasing the drawable's level with {@link
41  * android.graphics.drawable.Drawable#setLevel(int) setLevel()}.
42  * <p class="note"><strong>Note:</strong> The drawable is clipped completely and not visible when
43  * the level is 0 and fully revealed when the level is 10,000.</p>
44  *
45  * <p>It can be defined in an XML file with the <code>&lt;clip></code> element.  For more
46  * information, see the guide to <a
47  * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
48  *
49  * @attr ref android.R.styleable#ClipDrawable_clipOrientation
50  * @attr ref android.R.styleable#ClipDrawable_gravity
51  * @attr ref android.R.styleable#ClipDrawable_drawable
52  */
53 public class ClipDrawable extends Drawable implements Drawable.Callback {
54     private ClipState mState;
55     private final Rect mTmpRect = new Rect();
56 
57     public static final int HORIZONTAL = 1;
58     public static final int VERTICAL = 2;
59 
60     private boolean mMutated;
61 
ClipDrawable()62     ClipDrawable() {
63         this(null, null);
64     }
65 
66     /**
67      * @param orientation Bitwise-or of {@link #HORIZONTAL} and/or {@link #VERTICAL}
68      */
ClipDrawable(Drawable drawable, int gravity, int orientation)69     public ClipDrawable(Drawable drawable, int gravity, int orientation) {
70         this(null, null);
71 
72         mState.mDrawable = drawable;
73         mState.mGravity = gravity;
74         mState.mOrientation = orientation;
75 
76         if (drawable != null) {
77             drawable.setCallback(this);
78         }
79     }
80 
81     @Override
inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)82     public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
83             throws XmlPullParserException, IOException {
84         super.inflate(r, parser, attrs, theme);
85 
86         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ClipDrawable);
87 
88         // Reset mDrawable to preserve old multiple-inflate behavior. This is
89         // silly, but we have CTS tests that rely on it.
90         mState.mDrawable = null;
91 
92         updateStateFromTypedArray(a);
93         inflateChildElements(r, parser, attrs, theme);
94         verifyRequiredAttributes(a);
95         a.recycle();
96     }
97 
inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)98     private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
99             Theme theme) throws XmlPullParserException, IOException {
100         Drawable dr = null;
101         int type;
102         final int outerDepth = parser.getDepth();
103         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
104                 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
105             if (type != XmlPullParser.START_TAG) {
106                 continue;
107             }
108             dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
109         }
110 
111         if (dr != null) {
112             mState.mDrawable = dr;
113             dr.setCallback(this);
114         }
115     }
116 
verifyRequiredAttributes(TypedArray a)117     private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
118         // If we're not waiting on a theme, verify required attributes.
119         if (mState.mDrawable == null && (mState.mThemeAttrs == null
120                 || mState.mThemeAttrs[R.styleable.ClipDrawable_drawable] == 0)) {
121             throw new XmlPullParserException(a.getPositionDescription()
122                     + ": <clip> tag requires a 'drawable' attribute or "
123                     + "child tag defining a drawable");
124         }
125     }
126 
updateStateFromTypedArray(TypedArray a)127     private void updateStateFromTypedArray(TypedArray a) {
128         final ClipState state = mState;
129 
130         // Account for any configuration changes.
131         state.mChangingConfigurations |= a.getChangingConfigurations();
132 
133         // Extract the theme attributes, if any.
134         state.mThemeAttrs = a.extractThemeAttrs();
135 
136         state.mOrientation = a.getInt(R.styleable.ClipDrawable_clipOrientation, state.mOrientation);
137         state.mGravity = a.getInt(R.styleable.ClipDrawable_gravity, state.mGravity);
138 
139         final Drawable dr = a.getDrawable(R.styleable.ClipDrawable_drawable);
140         if (dr != null) {
141             state.mDrawable = dr;
142             dr.setCallback(this);
143         }
144     }
145 
146     @Override
applyTheme(Theme t)147     public void applyTheme(Theme t) {
148         super.applyTheme(t);
149 
150         final ClipState state = mState;
151         if (state == null || state.mThemeAttrs == null) {
152             return;
153         }
154 
155         final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ClipDrawable);
156         try {
157             updateStateFromTypedArray(a);
158             verifyRequiredAttributes(a);
159         } catch (XmlPullParserException e) {
160             throw new RuntimeException(e);
161         } finally {
162             a.recycle();
163         }
164 
165         if (state.mDrawable != null && state.mDrawable.canApplyTheme()) {
166             state.mDrawable.applyTheme(t);
167         }
168     }
169 
170     @Override
canApplyTheme()171     public boolean canApplyTheme() {
172         return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
173     }
174 
175     // overrides from Drawable.Callback
176 
177     @Override
invalidateDrawable(Drawable who)178     public void invalidateDrawable(Drawable who) {
179         final Callback callback = getCallback();
180         if (callback != null) {
181             callback.invalidateDrawable(this);
182         }
183     }
184 
185     @Override
scheduleDrawable(Drawable who, Runnable what, long when)186     public void scheduleDrawable(Drawable who, Runnable what, long when) {
187         final Callback callback = getCallback();
188         if (callback != null) {
189             callback.scheduleDrawable(this, what, when);
190         }
191     }
192 
193     @Override
unscheduleDrawable(Drawable who, Runnable what)194     public void unscheduleDrawable(Drawable who, Runnable what) {
195         final Callback callback = getCallback();
196         if (callback != null) {
197             callback.unscheduleDrawable(this, what);
198         }
199     }
200 
201     // overrides from Drawable
202 
203     @Override
getChangingConfigurations()204     public int getChangingConfigurations() {
205         return super.getChangingConfigurations()
206                 | mState.mChangingConfigurations
207                 | mState.mDrawable.getChangingConfigurations();
208     }
209 
210     @Override
getPadding(Rect padding)211     public boolean getPadding(Rect padding) {
212         // XXX need to adjust padding!
213         return mState.mDrawable.getPadding(padding);
214     }
215 
216     @Override
setVisible(boolean visible, boolean restart)217     public boolean setVisible(boolean visible, boolean restart) {
218         mState.mDrawable.setVisible(visible, restart);
219         return super.setVisible(visible, restart);
220     }
221 
222     @Override
setAlpha(int alpha)223     public void setAlpha(int alpha) {
224         mState.mDrawable.setAlpha(alpha);
225     }
226 
227     @Override
getAlpha()228     public int getAlpha() {
229         return mState.mDrawable.getAlpha();
230     }
231 
232     @Override
setColorFilter(ColorFilter cf)233     public void setColorFilter(ColorFilter cf) {
234         mState.mDrawable.setColorFilter(cf);
235     }
236 
237     @Override
setTintList(ColorStateList tint)238     public void setTintList(ColorStateList tint) {
239         mState.mDrawable.setTintList(tint);
240     }
241 
242     @Override
setTintMode(Mode tintMode)243     public void setTintMode(Mode tintMode) {
244         mState.mDrawable.setTintMode(tintMode);
245     }
246 
247     @Override
getOpacity()248     public int getOpacity() {
249         return mState.mDrawable.getOpacity();
250     }
251 
252     @Override
isStateful()253     public boolean isStateful() {
254         return mState.mDrawable.isStateful();
255     }
256 
257     @Override
onStateChange(int[] state)258     protected boolean onStateChange(int[] state) {
259         return mState.mDrawable.setState(state);
260     }
261 
262     @Override
onLevelChange(int level)263     protected boolean onLevelChange(int level) {
264         mState.mDrawable.setLevel(level);
265         invalidateSelf();
266         return true;
267     }
268 
269     @Override
onBoundsChange(Rect bounds)270     protected void onBoundsChange(Rect bounds) {
271         mState.mDrawable.setBounds(bounds);
272     }
273 
274     @Override
draw(Canvas canvas)275     public void draw(Canvas canvas) {
276 
277         if (mState.mDrawable.getLevel() == 0) {
278             return;
279         }
280 
281         final Rect r = mTmpRect;
282         final Rect bounds = getBounds();
283         int level = getLevel();
284         int w = bounds.width();
285         final int iw = 0; //mState.mDrawable.getIntrinsicWidth();
286         if ((mState.mOrientation & HORIZONTAL) != 0) {
287             w -= (w - iw) * (10000 - level) / 10000;
288         }
289         int h = bounds.height();
290         final int ih = 0; //mState.mDrawable.getIntrinsicHeight();
291         if ((mState.mOrientation & VERTICAL) != 0) {
292             h -= (h - ih) * (10000 - level) / 10000;
293         }
294         final int layoutDirection = getLayoutDirection();
295         Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);
296 
297         if (w > 0 && h > 0) {
298             canvas.save();
299             canvas.clipRect(r);
300             mState.mDrawable.draw(canvas);
301             canvas.restore();
302         }
303     }
304 
305     @Override
getIntrinsicWidth()306     public int getIntrinsicWidth() {
307         return mState.mDrawable.getIntrinsicWidth();
308     }
309 
310     @Override
getIntrinsicHeight()311     public int getIntrinsicHeight() {
312         return mState.mDrawable.getIntrinsicHeight();
313     }
314 
315     @Override
getConstantState()316     public ConstantState getConstantState() {
317         if (mState.canConstantState()) {
318             mState.mChangingConfigurations = getChangingConfigurations();
319             return mState;
320         }
321         return null;
322     }
323 
324     /** @hide */
325     @Override
setLayoutDirection(int layoutDirection)326     public void setLayoutDirection(int layoutDirection) {
327         mState.mDrawable.setLayoutDirection(layoutDirection);
328         super.setLayoutDirection(layoutDirection);
329     }
330 
331     @Override
mutate()332     public Drawable mutate() {
333         if (!mMutated && super.mutate() == this) {
334             mState.mDrawable.mutate();
335             mMutated = true;
336         }
337         return this;
338     }
339 
340     /**
341      * @hide
342      */
clearMutated()343     public void clearMutated() {
344         super.clearMutated();
345         mState.mDrawable.clearMutated();
346         mMutated = false;
347     }
348 
349     final static class ClipState extends ConstantState {
350         int[] mThemeAttrs;
351         int mChangingConfigurations;
352 
353         Drawable mDrawable;
354 
355         int mOrientation = HORIZONTAL;
356         int mGravity = Gravity.LEFT;
357 
358         private boolean mCheckedConstantState;
359         private boolean mCanConstantState;
360 
ClipState(ClipState orig, ClipDrawable owner, Resources res)361         ClipState(ClipState orig, ClipDrawable owner, Resources res) {
362             if (orig != null) {
363                 mThemeAttrs = orig.mThemeAttrs;
364                 mChangingConfigurations = orig.mChangingConfigurations;
365                 if (res != null) {
366                     mDrawable = orig.mDrawable.getConstantState().newDrawable(res);
367                 } else {
368                     mDrawable = orig.mDrawable.getConstantState().newDrawable();
369                 }
370                 mDrawable.setCallback(owner);
371                 mDrawable.setLayoutDirection(orig.mDrawable.getLayoutDirection());
372                 mDrawable.setBounds(orig.mDrawable.getBounds());
373                 mDrawable.setLevel(orig.mDrawable.getLevel());
374                 mOrientation = orig.mOrientation;
375                 mGravity = orig.mGravity;
376                 mCheckedConstantState = mCanConstantState = true;
377             }
378         }
379 
380         @Override
canApplyTheme()381         public boolean canApplyTheme() {
382             return mThemeAttrs != null || (mDrawable != null && mDrawable.canApplyTheme())
383                     || super.canApplyTheme();
384         }
385 
386         @Override
newDrawable()387         public Drawable newDrawable() {
388             return new ClipDrawable(this, null);
389         }
390 
391         @Override
newDrawable(Resources res)392         public Drawable newDrawable(Resources res) {
393             return new ClipDrawable(this, res);
394         }
395 
396         @Override
getChangingConfigurations()397         public int getChangingConfigurations() {
398             return mChangingConfigurations;
399         }
400 
canConstantState()401         boolean canConstantState() {
402             if (!mCheckedConstantState) {
403                 mCanConstantState = mDrawable.getConstantState() != null;
404                 mCheckedConstantState = true;
405             }
406 
407             return mCanConstantState;
408         }
409     }
410 
ClipDrawable(ClipState state, Resources res)411     private ClipDrawable(ClipState state, Resources res) {
412         mState = new ClipState(state, this, res);
413     }
414 }
415 
416