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