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.TypedArray;
28 import android.content.res.Resources.Theme;
29 import android.graphics.*;
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 DrawableWrapper {
54     public static final int HORIZONTAL = 1;
55     public static final int VERTICAL = 2;
56 
57     private static final int MAX_LEVEL = 10000;
58 
59     private final Rect mTmpRect = new Rect();
60 
61     private ClipState mState;
62 
ClipDrawable()63     ClipDrawable() {
64         this(new ClipState(null, null), null);
65     }
66 
67     /**
68      * Creates a new clip drawable with the specified gravity and orientation.
69      *
70      * @param drawable the drawable to clip
71      * @param gravity gravity constant (see {@link Gravity} used to position
72      *                the clipped drawable within the parent container
73      * @param orientation bitwise-or of {@link #HORIZONTAL} and/or
74      *                   {@link #VERTICAL}
75      */
ClipDrawable(Drawable drawable, int gravity, int orientation)76     public ClipDrawable(Drawable drawable, int gravity, int orientation) {
77         this(new ClipState(null, null), null);
78 
79         mState.mGravity = gravity;
80         mState.mOrientation = orientation;
81 
82         setDrawable(drawable);
83     }
84 
85     @Override
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)86     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
87             @NonNull AttributeSet attrs, @Nullable Theme theme)
88             throws XmlPullParserException, IOException {
89         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ClipDrawable);
90 
91         // Inflation will advance the XmlPullParser and AttributeSet.
92         super.inflate(r, parser, attrs, theme);
93 
94         updateStateFromTypedArray(a);
95         verifyRequiredAttributes(a);
96         a.recycle();
97     }
98 
99     @Override
applyTheme(@onNull Theme t)100     public void applyTheme(@NonNull Theme t) {
101         super.applyTheme(t);
102 
103         final ClipState state = mState;
104         if (state == null) {
105             return;
106         }
107 
108         if (state.mThemeAttrs != null) {
109             final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ClipDrawable);
110             try {
111                 updateStateFromTypedArray(a);
112                 verifyRequiredAttributes(a);
113             } catch (XmlPullParserException e) {
114                 rethrowAsRuntimeException(e);
115             } finally {
116                 a.recycle();
117             }
118         }
119     }
120 
verifyRequiredAttributes(@onNull TypedArray a)121     private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
122         // If we're not waiting on a theme, verify required attributes.
123         if (getDrawable() == null && (mState.mThemeAttrs == null
124                 || mState.mThemeAttrs[R.styleable.ClipDrawable_drawable] == 0)) {
125             throw new XmlPullParserException(a.getPositionDescription()
126                     + ": <clip> tag requires a 'drawable' attribute or "
127                     + "child tag defining a drawable");
128         }
129     }
130 
updateStateFromTypedArray(@onNull TypedArray a)131     private void updateStateFromTypedArray(@NonNull TypedArray a) {
132         final ClipState state = mState;
133         if (state == null) {
134             return;
135         }
136 
137         // Account for any configuration changes.
138         state.mChangingConfigurations |= a.getChangingConfigurations();
139 
140         // Extract the theme attributes, if any.
141         state.mThemeAttrs = a.extractThemeAttrs();
142 
143         state.mOrientation = a.getInt(
144                 R.styleable.ClipDrawable_clipOrientation, state.mOrientation);
145         state.mGravity = a.getInt(
146                 R.styleable.ClipDrawable_gravity, state.mGravity);
147     }
148 
149     @Override
onLevelChange(int level)150     protected boolean onLevelChange(int level) {
151         super.onLevelChange(level);
152         invalidateSelf();
153         return true;
154     }
155 
156     @Override
getOpacity()157     public int getOpacity() {
158         final Drawable dr = getDrawable();
159         final int opacity = dr.getOpacity();
160         if (opacity == PixelFormat.TRANSPARENT || dr.getLevel() == 0) {
161             return PixelFormat.TRANSPARENT;
162         }
163 
164         final int level = getLevel();
165         if (level >= MAX_LEVEL) {
166             return dr.getOpacity();
167         }
168 
169         // Some portion of non-transparent drawable is showing.
170         return PixelFormat.TRANSLUCENT;
171     }
172 
173     @Override
draw(Canvas canvas)174     public void draw(Canvas canvas) {
175         final Drawable dr = getDrawable();
176         if (dr.getLevel() == 0) {
177             return;
178         }
179 
180         final Rect r = mTmpRect;
181         final Rect bounds = getBounds();
182         final int level = getLevel();
183 
184         int w = bounds.width();
185         final int iw = 0; //mState.mDrawable.getIntrinsicWidth();
186         if ((mState.mOrientation & HORIZONTAL) != 0) {
187             w -= (w - iw) * (MAX_LEVEL - level) / MAX_LEVEL;
188         }
189 
190         int h = bounds.height();
191         final int ih = 0; //mState.mDrawable.getIntrinsicHeight();
192         if ((mState.mOrientation & VERTICAL) != 0) {
193             h -= (h - ih) * (MAX_LEVEL - level) / MAX_LEVEL;
194         }
195 
196         final int layoutDirection = getLayoutDirection();
197         Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);
198 
199         if (w > 0 && h > 0) {
200             canvas.save();
201             canvas.clipRect(r);
202             dr.draw(canvas);
203             canvas.restore();
204         }
205     }
206 
207     @Override
mutateConstantState()208     DrawableWrapperState mutateConstantState() {
209         mState = new ClipState(mState, null);
210         return mState;
211     }
212 
213     static final class ClipState extends DrawableWrapper.DrawableWrapperState {
214         private int[] mThemeAttrs;
215 
216         int mOrientation = HORIZONTAL;
217         int mGravity = Gravity.LEFT;
218 
ClipState(ClipState orig, Resources res)219         ClipState(ClipState orig, Resources res) {
220             super(orig, res);
221 
222             if (orig != null) {
223                 mOrientation = orig.mOrientation;
224                 mGravity = orig.mGravity;
225             }
226         }
227 
228         @Override
newDrawable(Resources res)229         public Drawable newDrawable(Resources res) {
230             return new ClipDrawable(this, res);
231         }
232     }
233 
ClipDrawable(ClipState state, Resources res)234     private ClipDrawable(ClipState state, Resources res) {
235         super(state, res);
236 
237         mState = state;
238     }
239 }
240 
241