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
17package android.graphics.drawable;
18
19import com.android.internal.R;
20
21import org.xmlpull.v1.XmlPullParser;
22import org.xmlpull.v1.XmlPullParserException;
23
24import android.content.res.Resources;
25import android.content.res.TypedArray;
26import android.content.res.Resources.Theme;
27import android.graphics.*;
28import android.view.Gravity;
29import android.util.AttributeSet;
30
31import java.io.IOException;
32
33/**
34 * A Drawable that clips another Drawable based on this Drawable's current
35 * level value.  You can control how much the child Drawable gets clipped in width
36 * and height based on the level, as well as a gravity to control where it is
37 * placed in its overall container.  Most often used to implement things like
38 * progress bars, by increasing the drawable's level with {@link
39 * android.graphics.drawable.Drawable#setLevel(int) setLevel()}.
40 * <p class="note"><strong>Note:</strong> The drawable is clipped completely and not visible when
41 * the level is 0 and fully revealed when the level is 10,000.</p>
42 *
43 * <p>It can be defined in an XML file with the <code>&lt;clip></code> element.  For more
44 * information, see the guide to <a
45 * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
46 *
47 * @attr ref android.R.styleable#ClipDrawable_clipOrientation
48 * @attr ref android.R.styleable#ClipDrawable_gravity
49 * @attr ref android.R.styleable#ClipDrawable_drawable
50 */
51public class ClipDrawable extends DrawableWrapper {
52    public static final int HORIZONTAL = 1;
53    public static final int VERTICAL = 2;
54
55    private static final int MAX_LEVEL = 10000;
56
57    private final Rect mTmpRect = new Rect();
58
59    private ClipState mState;
60
61    ClipDrawable() {
62        this(new ClipState(null), null);
63    }
64
65    /**
66     * Creates a new clip drawable with the specified gravity and orientation.
67     *
68     * @param drawable the drawable to clip
69     * @param gravity gravity constant (see {@link Gravity} used to position
70     *                the clipped drawable within the parent container
71     * @param orientation bitwise-or of {@link #HORIZONTAL} and/or
72     *                   {@link #VERTICAL}
73     */
74    public ClipDrawable(Drawable drawable, int gravity, int orientation) {
75        this(new ClipState(null), null);
76
77        mState.mGravity = gravity;
78        mState.mOrientation = orientation;
79
80        setDrawable(drawable);
81    }
82
83    @Override
84    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
85            throws XmlPullParserException, IOException {
86        super.inflate(r, parser, attrs, theme);
87
88        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.ClipDrawable);
89        updateStateFromTypedArray(a);
90        inflateChildDrawable(r, parser, attrs, theme);
91        verifyRequiredAttributes(a);
92        a.recycle();
93    }
94
95    private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
96        // If we're not waiting on a theme, verify required attributes.
97        if (getDrawable() == null && (mState.mThemeAttrs == null
98                || mState.mThemeAttrs[R.styleable.ClipDrawable_drawable] == 0)) {
99            throw new XmlPullParserException(a.getPositionDescription()
100                    + ": <clip> tag requires a 'drawable' attribute or "
101                    + "child tag defining a drawable");
102        }
103    }
104
105    @Override
106    void updateStateFromTypedArray(TypedArray a) {
107        super.updateStateFromTypedArray(a);
108
109        final ClipState state = mState;
110        state.mOrientation = a.getInt(
111                R.styleable.ClipDrawable_clipOrientation, state.mOrientation);
112        state.mGravity = a.getInt(
113                R.styleable.ClipDrawable_gravity, state.mGravity);
114
115        final Drawable dr = a.getDrawable(R.styleable.ClipDrawable_drawable);
116        if (dr != null) {
117            setDrawable(dr);
118        }
119    }
120
121    @Override
122    public void applyTheme(Theme t) {
123        final ClipState state = mState;
124        if (state == null) {
125            return;
126        }
127
128        if (state.mThemeAttrs != null) {
129            final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.ClipDrawable);
130            try {
131                updateStateFromTypedArray(a);
132                verifyRequiredAttributes(a);
133            } catch (XmlPullParserException e) {
134                throw new RuntimeException(e);
135            } finally {
136                a.recycle();
137            }
138        }
139
140        // The drawable may have changed as a result of applying the theme, so
141        // apply the theme to the wrapped drawable last.
142        super.applyTheme(t);
143    }
144
145    @Override
146    protected boolean onLevelChange(int level) {
147        super.onLevelChange(level);
148        invalidateSelf();
149        return true;
150    }
151
152    @Override
153    public int getOpacity() {
154        final Drawable dr = getDrawable();
155        final int opacity = dr.getOpacity();
156        if (opacity == PixelFormat.TRANSPARENT || dr.getLevel() == 0) {
157            return PixelFormat.TRANSPARENT;
158        }
159
160        final int level = getLevel();
161        if (level >= MAX_LEVEL) {
162            return dr.getOpacity();
163        }
164
165        // Some portion of non-transparent drawable is showing.
166        return PixelFormat.TRANSLUCENT;
167    }
168
169    @Override
170    public void draw(Canvas canvas) {
171        final Drawable dr = getDrawable();
172        if (dr.getLevel() == 0) {
173            return;
174        }
175
176        final Rect r = mTmpRect;
177        final Rect bounds = getBounds();
178        final int level = getLevel();
179
180        int w = bounds.width();
181        final int iw = 0; //mState.mDrawable.getIntrinsicWidth();
182        if ((mState.mOrientation & HORIZONTAL) != 0) {
183            w -= (w - iw) * (MAX_LEVEL - level) / MAX_LEVEL;
184        }
185
186        int h = bounds.height();
187        final int ih = 0; //mState.mDrawable.getIntrinsicHeight();
188        if ((mState.mOrientation & VERTICAL) != 0) {
189            h -= (h - ih) * (MAX_LEVEL - level) / MAX_LEVEL;
190        }
191
192        final int layoutDirection = getLayoutDirection();
193        Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);
194
195        if (w > 0 && h > 0) {
196            canvas.save();
197            canvas.clipRect(r);
198            dr.draw(canvas);
199            canvas.restore();
200        }
201    }
202
203    static final class ClipState extends DrawableWrapper.DrawableWrapperState {
204        int mOrientation = HORIZONTAL;
205        int mGravity = Gravity.LEFT;
206
207        ClipState(ClipState orig) {
208            super(orig);
209
210            if (orig != null) {
211                mOrientation = orig.mOrientation;
212                mGravity = orig.mGravity;
213            }
214        }
215
216        @Override
217        public Drawable newDrawable(Resources res) {
218            return new ClipDrawable(this, res);
219        }
220    }
221
222    private ClipDrawable(ClipState state, Resources res) {
223        super(state, res);
224
225        mState = state;
226    }
227}
228
229