1/*
2 * Copyright (C) 2008 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 android.annotation.NonNull;
22
23import org.xmlpull.v1.XmlPullParser;
24import org.xmlpull.v1.XmlPullParserException;
25
26import android.content.res.ColorStateList;
27import android.content.res.Resources;
28import android.content.res.TypedArray;
29import android.content.res.Resources.Theme;
30import android.graphics.Bitmap;
31import android.graphics.Canvas;
32import android.graphics.ColorFilter;
33import android.graphics.Insets;
34import android.graphics.Outline;
35import android.graphics.PixelFormat;
36import android.graphics.PorterDuff.Mode;
37import android.graphics.drawable.Drawable.ConstantState;
38import android.graphics.Rect;
39import android.util.AttributeSet;
40
41import java.io.IOException;
42import java.util.Collection;
43
44/**
45 * A Drawable that insets another Drawable by a specified distance.
46 * This is used when a View needs a background that is smaller than
47 * the View's actual bounds.
48 *
49 * <p>It can be defined in an XML file with the <code>&lt;inset></code> element. For more
50 * information, see the guide to <a
51 * href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.</p>
52 *
53 * @attr ref android.R.styleable#InsetDrawable_visible
54 * @attr ref android.R.styleable#InsetDrawable_drawable
55 * @attr ref android.R.styleable#InsetDrawable_insetLeft
56 * @attr ref android.R.styleable#InsetDrawable_insetRight
57 * @attr ref android.R.styleable#InsetDrawable_insetTop
58 * @attr ref android.R.styleable#InsetDrawable_insetBottom
59 */
60public class InsetDrawable extends Drawable implements Drawable.Callback {
61    private final Rect mTmpRect = new Rect();
62
63    private final InsetState mState;
64
65    private boolean mMutated;
66
67    /*package*/ InsetDrawable() {
68        this(null, null);
69    }
70
71    public InsetDrawable(Drawable drawable, int inset) {
72        this(drawable, inset, inset, inset, inset);
73    }
74
75    public InsetDrawable(Drawable drawable, int insetLeft, int insetTop,
76                         int insetRight, int insetBottom) {
77        this(null, null);
78
79        mState.mDrawable = drawable;
80        mState.mInsetLeft = insetLeft;
81        mState.mInsetTop = insetTop;
82        mState.mInsetRight = insetRight;
83        mState.mInsetBottom = insetBottom;
84
85        if (drawable != null) {
86            drawable.setCallback(this);
87        }
88    }
89
90    @Override
91    public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
92            throws XmlPullParserException, IOException {
93        final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.InsetDrawable);
94        super.inflateWithAttributes(r, parser, a, R.styleable.InsetDrawable_visible);
95
96        // Reset mDrawable to preserve old multiple-inflate behavior. This is
97        // silly, but we have CTS tests that rely on it.
98        mState.mDrawable = null;
99
100        updateStateFromTypedArray(a);
101        inflateChildElements(r, parser, attrs, theme);
102        verifyRequiredAttributes(a);
103        a.recycle();
104    }
105
106    private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
107            Theme theme) throws XmlPullParserException, IOException {
108        // Load inner XML elements.
109        if (mState.mDrawable == null) {
110            int type;
111            while ((type=parser.next()) == XmlPullParser.TEXT) {
112            }
113            if (type != XmlPullParser.START_TAG) {
114                throw new XmlPullParserException(
115                        parser.getPositionDescription()
116                                + ": <inset> tag requires a 'drawable' attribute or "
117                                + "child tag defining a drawable");
118            }
119            final Drawable dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
120            mState.mDrawable = dr;
121            dr.setCallback(this);
122        }
123    }
124
125    private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
126        // If we're not waiting on a theme, verify required attributes.
127        if (mState.mDrawable == null && (mState.mThemeAttrs == null
128                || mState.mThemeAttrs[R.styleable.InsetDrawable_drawable] == 0)) {
129            throw new XmlPullParserException(a.getPositionDescription()
130                    + ": <inset> tag requires a 'drawable' attribute or "
131                    + "child tag defining a drawable");
132        }
133    }
134
135    private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
136        final InsetState state = mState;
137
138        // Account for any configuration changes.
139        state.mChangingConfigurations |= a.getChangingConfigurations();
140
141        // Extract the theme attributes, if any.
142        state.mThemeAttrs = a.extractThemeAttrs();
143
144        final int N = a.getIndexCount();
145        for (int i = 0; i < N; i++) {
146            final int attr = a.getIndex(i);
147            switch (attr) {
148                case R.styleable.InsetDrawable_drawable:
149                    final Drawable dr = a.getDrawable(attr);
150                    if (dr != null) {
151                        state.mDrawable = dr;
152                        dr.setCallback(this);
153                    }
154                    break;
155                case R.styleable.InsetDrawable_inset:
156                    final int inset = a.getDimensionPixelOffset(attr, Integer.MIN_VALUE);
157                    if (inset != Integer.MIN_VALUE) {
158                        state.mInsetLeft = inset;
159                        state.mInsetTop = inset;
160                        state.mInsetRight = inset;
161                        state.mInsetBottom = inset;
162                    }
163                    break;
164                case R.styleable.InsetDrawable_insetLeft:
165                    state.mInsetLeft = a.getDimensionPixelOffset(attr, state.mInsetLeft);
166                    break;
167                case R.styleable.InsetDrawable_insetTop:
168                    state.mInsetTop = a.getDimensionPixelOffset(attr, state.mInsetTop);
169                    break;
170                case R.styleable.InsetDrawable_insetRight:
171                    state.mInsetRight = a.getDimensionPixelOffset(attr, state.mInsetRight);
172                    break;
173                case R.styleable.InsetDrawable_insetBottom:
174                    state.mInsetBottom = a.getDimensionPixelOffset(attr, state.mInsetBottom);
175                    break;
176            }
177        }
178    }
179
180    @Override
181    public void applyTheme(Theme t) {
182        super.applyTheme(t);
183
184        final InsetState state = mState;
185        if (state == null) {
186            return;
187        }
188
189        if (state.mThemeAttrs != null) {
190            final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.InsetDrawable);
191            try {
192                updateStateFromTypedArray(a);
193                verifyRequiredAttributes(a);
194            } catch (XmlPullParserException e) {
195                throw new RuntimeException(e);
196            } finally {
197                a.recycle();
198            }
199        }
200
201        if (state.mDrawable != null && state.mDrawable.canApplyTheme()) {
202            state.mDrawable.applyTheme(t);
203        }
204    }
205
206    @Override
207    public boolean canApplyTheme() {
208        return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
209    }
210
211    @Override
212    public void invalidateDrawable(Drawable who) {
213        final Callback callback = getCallback();
214        if (callback != null) {
215            callback.invalidateDrawable(this);
216        }
217    }
218
219    @Override
220    public void scheduleDrawable(Drawable who, Runnable what, long when) {
221        final Callback callback = getCallback();
222        if (callback != null) {
223            callback.scheduleDrawable(this, what, when);
224        }
225    }
226
227    @Override
228    public void unscheduleDrawable(Drawable who, Runnable what) {
229        final Callback callback = getCallback();
230        if (callback != null) {
231            callback.unscheduleDrawable(this, what);
232        }
233    }
234
235    @Override
236    public void draw(Canvas canvas) {
237        mState.mDrawable.draw(canvas);
238    }
239
240    @Override
241    public int getChangingConfigurations() {
242        return super.getChangingConfigurations()
243                | mState.mChangingConfigurations
244                | mState.mDrawable.getChangingConfigurations();
245    }
246
247    @Override
248    public boolean getPadding(Rect padding) {
249        boolean pad = mState.mDrawable.getPadding(padding);
250
251        padding.left += mState.mInsetLeft;
252        padding.right += mState.mInsetRight;
253        padding.top += mState.mInsetTop;
254        padding.bottom += mState.mInsetBottom;
255
256        return pad || (mState.mInsetLeft | mState.mInsetRight |
257                mState.mInsetTop | mState.mInsetBottom) != 0;
258    }
259
260    /** @hide */
261    @Override
262    public Insets getOpticalInsets() {
263        final Insets contentInsets = super.getOpticalInsets();
264        return Insets.of(contentInsets.left + mState.mInsetLeft,
265                contentInsets.top + mState.mInsetTop,
266                contentInsets.right + mState.mInsetRight,
267                contentInsets.bottom + mState.mInsetBottom);
268    }
269
270    @Override
271    public void setHotspot(float x, float y) {
272        mState.mDrawable.setHotspot(x, y);
273    }
274
275    @Override
276    public void setHotspotBounds(int left, int top, int right, int bottom) {
277        mState.mDrawable.setHotspotBounds(left, top, right, bottom);
278    }
279
280    /** @hide */
281    @Override
282    public void getHotspotBounds(Rect outRect) {
283        mState.mDrawable.getHotspotBounds(outRect);
284    }
285
286    @Override
287    public boolean setVisible(boolean visible, boolean restart) {
288        mState.mDrawable.setVisible(visible, restart);
289        return super.setVisible(visible, restart);
290    }
291
292    @Override
293    public void setAlpha(int alpha) {
294        mState.mDrawable.setAlpha(alpha);
295    }
296
297    @Override
298    public int getAlpha() {
299        return mState.mDrawable.getAlpha();
300    }
301
302    @Override
303    public void setColorFilter(ColorFilter cf) {
304        mState.mDrawable.setColorFilter(cf);
305    }
306
307    @Override
308    public void setTintList(ColorStateList tint) {
309        mState.mDrawable.setTintList(tint);
310    }
311
312    @Override
313    public void setTintMode(Mode tintMode) {
314        mState.mDrawable.setTintMode(tintMode);
315    }
316
317    /** {@hide} */
318    @Override
319    public void setLayoutDirection(int layoutDirection) {
320        mState.mDrawable.setLayoutDirection(layoutDirection);
321    }
322
323    @Override
324    public int getOpacity() {
325        final InsetState state = mState;
326        final int opacity = state.mDrawable.getOpacity();
327        if (opacity == PixelFormat.OPAQUE && (state.mInsetLeft > 0 || state.mInsetTop > 0
328                || state.mInsetRight > 0 || state.mInsetBottom > 0)) {
329            return PixelFormat.TRANSLUCENT;
330        }
331        return opacity;
332    }
333
334    @Override
335    public boolean isStateful() {
336        return mState.mDrawable.isStateful();
337    }
338
339    @Override
340    protected boolean onStateChange(int[] state) {
341        boolean changed = mState.mDrawable.setState(state);
342        onBoundsChange(getBounds());
343        return changed;
344    }
345
346    @Override
347    protected boolean onLevelChange(int level) {
348        return mState.mDrawable.setLevel(level);
349    }
350
351    @Override
352    protected void onBoundsChange(Rect bounds) {
353        final Rect r = mTmpRect;
354        r.set(bounds);
355
356        r.left += mState.mInsetLeft;
357        r.top += mState.mInsetTop;
358        r.right -= mState.mInsetRight;
359        r.bottom -= mState.mInsetBottom;
360
361        mState.mDrawable.setBounds(r.left, r.top, r.right, r.bottom);
362    }
363
364    @Override
365    public int getIntrinsicWidth() {
366        return mState.mDrawable.getIntrinsicWidth()
367                + mState.mInsetLeft + mState.mInsetRight;
368    }
369
370    @Override
371    public int getIntrinsicHeight() {
372        return mState.mDrawable.getIntrinsicHeight()
373                + mState.mInsetTop + mState.mInsetBottom;
374    }
375
376    @Override
377    public void getOutline(@NonNull Outline outline) {
378        mState.mDrawable.getOutline(outline);
379    }
380
381    @Override
382    public ConstantState getConstantState() {
383        if (mState.canConstantState()) {
384            mState.mChangingConfigurations = getChangingConfigurations();
385            return mState;
386        }
387        return null;
388    }
389
390    @Override
391    public Drawable mutate() {
392        if (!mMutated && super.mutate() == this) {
393            mState.mDrawable.mutate();
394            mMutated = true;
395        }
396        return this;
397    }
398
399    /**
400     * @hide
401     */
402    public void clearMutated() {
403        super.clearMutated();
404        mState.mDrawable.clearMutated();
405        mMutated = false;
406    }
407
408    /**
409     * Returns the drawable wrapped by this InsetDrawable. May be null.
410     */
411    public Drawable getDrawable() {
412        return mState.mDrawable;
413    }
414
415    final static class InsetState extends ConstantState {
416        int[] mThemeAttrs;
417        int mChangingConfigurations;
418
419        Drawable mDrawable;
420
421        int mInsetLeft = 0;
422        int mInsetTop = 0;
423        int mInsetRight = 0;
424        int mInsetBottom = 0;
425
426        private boolean mCheckedConstantState;
427        private boolean mCanConstantState;
428
429        InsetState(InsetState orig, InsetDrawable owner, Resources res) {
430            if (orig != null) {
431                mThemeAttrs = orig.mThemeAttrs;
432                mChangingConfigurations = orig.mChangingConfigurations;
433                if (res != null) {
434                    mDrawable = orig.mDrawable.getConstantState().newDrawable(res);
435                } else {
436                    mDrawable = orig.mDrawable.getConstantState().newDrawable();
437                }
438                mDrawable.setCallback(owner);
439                mDrawable.setLayoutDirection(orig.mDrawable.getLayoutDirection());
440                mDrawable.setBounds(orig.mDrawable.getBounds());
441                mDrawable.setLevel(orig.mDrawable.getLevel());
442                mInsetLeft = orig.mInsetLeft;
443                mInsetTop = orig.mInsetTop;
444                mInsetRight = orig.mInsetRight;
445                mInsetBottom = orig.mInsetBottom;
446                mCheckedConstantState = mCanConstantState = true;
447            }
448        }
449
450        @Override
451        public boolean canApplyTheme() {
452            return mThemeAttrs != null || (mDrawable != null && mDrawable.canApplyTheme())
453                    || super.canApplyTheme();
454        }
455
456        @Override
457        public Drawable newDrawable() {
458            return new InsetDrawable(this, null);
459        }
460
461        @Override
462        public Drawable newDrawable(Resources res) {
463            return new InsetDrawable(this, res);
464        }
465
466        @Override
467        public int getChangingConfigurations() {
468            return mChangingConfigurations;
469        }
470
471        boolean canConstantState() {
472            if (!mCheckedConstantState) {
473                mCanConstantState = mDrawable.getConstantState() != null;
474                mCheckedConstantState = true;
475            }
476
477            return mCanConstantState;
478        }
479
480        @Override
481        public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
482            final ConstantState state = mDrawable.getConstantState();
483            if (state != null) {
484                return state.addAtlasableBitmaps(atlasList);
485            }
486            return 0;
487        }
488    }
489
490    private InsetDrawable(InsetState state, Resources res) {
491        mState = new InsetState(state, this, res);
492    }
493}
494
495