InsetDrawable.java revision bc43ac2094a833623e99c38cd3d0cedfef61260e
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 setDrawable(Drawable dr) {
123        if (mDrawable != null) {
124            mDrawable.setCallback(null);
125        }
126
127        mDrawable = dr;
128
129        if (dr != null) {
130            dr.setCallback(this);
131            dr.setVisible(isVisible(), true);
132            dr.setState(getState());
133            dr.setLevel(getLevel());
134            dr.setBounds(getBounds());
135            dr.setLayoutDirection(getLayoutDirection());
136        }
137    }
138
139    private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
140            Theme theme) throws XmlPullParserException, IOException {
141        // Load inner XML elements.
142        if (mDrawable == null) {
143            int type;
144            while ((type=parser.next()) == XmlPullParser.TEXT) {
145            }
146            if (type != XmlPullParser.START_TAG) {
147                throw new XmlPullParserException(parser.getPositionDescription()
148                        + ": <inset> tag requires a 'drawable' attribute or "
149                        + "child tag defining a drawable");
150            }
151
152            final Drawable dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
153            if (dr != null) {
154                mState.mDrawableState = dr.getConstantState();
155                setDrawable(dr);
156            }
157        }
158    }
159
160    private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException {
161        // If we're not waiting on a theme, verify required attributes.
162        if (mDrawable == null && (mState.mThemeAttrs == null
163                || mState.mThemeAttrs[R.styleable.InsetDrawable_drawable] == 0)) {
164            throw new XmlPullParserException(a.getPositionDescription()
165                    + ": <inset> tag requires a 'drawable' attribute or "
166                    + "child tag defining a drawable");
167        }
168    }
169
170    private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException {
171        final InsetState state = mState;
172
173        // Account for any configuration changes.
174        state.mChangingConfigurations |= a.getChangingConfigurations();
175
176        // Extract the theme attributes, if any.
177        state.mThemeAttrs = a.extractThemeAttrs();
178
179        final int N = a.getIndexCount();
180        for (int i = 0; i < N; i++) {
181            final int attr = a.getIndex(i);
182            switch (attr) {
183                case R.styleable.InsetDrawable_drawable:
184                    final Drawable dr = a.getDrawable(attr);
185                    if (dr != null) {
186                        mState.mDrawableState = dr.getConstantState();
187                        setDrawable(dr);
188                    }
189                    break;
190                case R.styleable.InsetDrawable_inset:
191                    final int inset = a.getDimensionPixelOffset(attr, Integer.MIN_VALUE);
192                    if (inset != Integer.MIN_VALUE) {
193                        state.mInsetLeft = inset;
194                        state.mInsetTop = inset;
195                        state.mInsetRight = inset;
196                        state.mInsetBottom = inset;
197                    }
198                    break;
199                case R.styleable.InsetDrawable_insetLeft:
200                    state.mInsetLeft = a.getDimensionPixelOffset(attr, state.mInsetLeft);
201                    break;
202                case R.styleable.InsetDrawable_insetTop:
203                    state.mInsetTop = a.getDimensionPixelOffset(attr, state.mInsetTop);
204                    break;
205                case R.styleable.InsetDrawable_insetRight:
206                    state.mInsetRight = a.getDimensionPixelOffset(attr, state.mInsetRight);
207                    break;
208                case R.styleable.InsetDrawable_insetBottom:
209                    state.mInsetBottom = a.getDimensionPixelOffset(attr, state.mInsetBottom);
210                    break;
211            }
212        }
213    }
214
215    @Override
216    public void applyTheme(Theme t) {
217        super.applyTheme(t);
218
219        final InsetState state = mState;
220        if (state == null) {
221            return;
222        }
223
224        if (state.mThemeAttrs != null) {
225            final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.InsetDrawable);
226            try {
227                updateStateFromTypedArray(a);
228                verifyRequiredAttributes(a);
229            } catch (XmlPullParserException e) {
230                throw new RuntimeException(e);
231            } finally {
232                a.recycle();
233            }
234        }
235
236        if (mDrawable != null && mDrawable.canApplyTheme()) {
237            mDrawable.applyTheme(t);
238        }
239    }
240
241    @Override
242    public boolean canApplyTheme() {
243        return (mState != null && mState.canApplyTheme()) || super.canApplyTheme();
244    }
245
246    @Override
247    public void invalidateDrawable(Drawable who) {
248        final Callback callback = getCallback();
249        if (callback != null) {
250            callback.invalidateDrawable(this);
251        }
252    }
253
254    @Override
255    public void scheduleDrawable(Drawable who, Runnable what, long when) {
256        final Callback callback = getCallback();
257        if (callback != null) {
258            callback.scheduleDrawable(this, what, when);
259        }
260    }
261
262    @Override
263    public void unscheduleDrawable(Drawable who, Runnable what) {
264        final Callback callback = getCallback();
265        if (callback != null) {
266            callback.unscheduleDrawable(this, what);
267        }
268    }
269
270    @Override
271    public void draw(Canvas canvas) {
272        mDrawable.draw(canvas);
273    }
274
275    @Override
276    public int getChangingConfigurations() {
277        return super.getChangingConfigurations()
278                | mState.mChangingConfigurations
279                | mDrawable.getChangingConfigurations();
280    }
281
282    @Override
283    public boolean getPadding(Rect padding) {
284        final boolean pad = mDrawable.getPadding(padding);
285
286        padding.left += mState.mInsetLeft;
287        padding.right += mState.mInsetRight;
288        padding.top += mState.mInsetTop;
289        padding.bottom += mState.mInsetBottom;
290
291        return pad || (mState.mInsetLeft | mState.mInsetRight
292                | mState.mInsetTop | mState.mInsetBottom) != 0;
293    }
294
295    /** @hide */
296    @Override
297    public Insets getOpticalInsets() {
298        final Insets contentInsets = super.getOpticalInsets();
299        return Insets.of(contentInsets.left + mState.mInsetLeft,
300                contentInsets.top + mState.mInsetTop,
301                contentInsets.right + mState.mInsetRight,
302                contentInsets.bottom + mState.mInsetBottom);
303    }
304
305    @Override
306    public void setHotspot(float x, float y) {
307        mDrawable.setHotspot(x, y);
308    }
309
310    @Override
311    public void setHotspotBounds(int left, int top, int right, int bottom) {
312        mDrawable.setHotspotBounds(left, top, right, bottom);
313    }
314
315    /** @hide */
316    @Override
317    public void getHotspotBounds(Rect outRect) {
318        mDrawable.getHotspotBounds(outRect);
319    }
320
321    @Override
322    public boolean setVisible(boolean visible, boolean restart) {
323        mDrawable.setVisible(visible, restart);
324        return super.setVisible(visible, restart);
325    }
326
327    @Override
328    public void setAlpha(int alpha) {
329        mDrawable.setAlpha(alpha);
330    }
331
332    @Override
333    public int getAlpha() {
334        return mDrawable.getAlpha();
335    }
336
337    @Override
338    public void setColorFilter(ColorFilter cf) {
339        mDrawable.setColorFilter(cf);
340    }
341
342    @Override
343    public void setTintList(ColorStateList tint) {
344        mDrawable.setTintList(tint);
345    }
346
347    @Override
348    public void setTintMode(Mode tintMode) {
349        mDrawable.setTintMode(tintMode);
350    }
351
352    /** {@hide} */
353    @Override
354    public void setLayoutDirection(int layoutDirection) {
355        mDrawable.setLayoutDirection(layoutDirection);
356    }
357
358    @Override
359    public int getOpacity() {
360        final InsetState state = mState;
361        final int opacity = mDrawable.getOpacity();
362        if (opacity == PixelFormat.OPAQUE && (state.mInsetLeft > 0 || state.mInsetTop > 0
363                || state.mInsetRight > 0 || state.mInsetBottom > 0)) {
364            return PixelFormat.TRANSLUCENT;
365        }
366        return opacity;
367    }
368
369    @Override
370    public boolean isStateful() {
371        return mDrawable.isStateful();
372    }
373
374    @Override
375    protected boolean onStateChange(int[] state) {
376        final boolean changed = mDrawable.setState(state);
377        onBoundsChange(getBounds());
378        return changed;
379    }
380
381    @Override
382    protected boolean onLevelChange(int level) {
383        return mDrawable.setLevel(level);
384    }
385
386    @Override
387    protected void onBoundsChange(Rect bounds) {
388        final Rect r = mTmpRect;
389        r.set(bounds);
390
391        r.left += mState.mInsetLeft;
392        r.top += mState.mInsetTop;
393        r.right -= mState.mInsetRight;
394        r.bottom -= mState.mInsetBottom;
395
396        mDrawable.setBounds(r.left, r.top, r.right, r.bottom);
397    }
398
399    @Override
400    public int getIntrinsicWidth() {
401        return mDrawable.getIntrinsicWidth() + mState.mInsetLeft + mState.mInsetRight;
402    }
403
404    @Override
405    public int getIntrinsicHeight() {
406        return mDrawable.getIntrinsicHeight() + mState.mInsetTop + mState.mInsetBottom;
407    }
408
409    @Override
410    public void getOutline(@NonNull Outline outline) {
411        mDrawable.getOutline(outline);
412    }
413
414    @Override
415    public ConstantState getConstantState() {
416        if (mState.canConstantState()) {
417            mState.mChangingConfigurations = getChangingConfigurations();
418            return mState;
419        }
420        return null;
421    }
422
423    @Override
424    public Drawable mutate() {
425        if (!mMutated && super.mutate() == this) {
426            mState = new InsetState(mState);
427            mDrawable.mutate();
428            mMutated = true;
429        }
430        return this;
431    }
432
433    /**
434     * @hide
435     */
436    public void clearMutated() {
437        super.clearMutated();
438        mDrawable.clearMutated();
439        mMutated = false;
440    }
441
442    /**
443     * Returns the drawable wrapped by this InsetDrawable. May be null.
444     */
445    public Drawable getDrawable() {
446        return mDrawable;
447    }
448
449    private static final class InsetState extends ConstantState {
450        int[] mThemeAttrs;
451        int mChangingConfigurations;
452
453        ConstantState mDrawableState;
454
455        int mInsetLeft = 0;
456        int mInsetTop = 0;
457        int mInsetRight = 0;
458        int mInsetBottom = 0;
459
460        public InsetState() {
461            // Empty constructor.
462        }
463
464        public InsetState(InsetState orig) {
465            if (orig != null) {
466                mThemeAttrs = orig.mThemeAttrs;
467                mChangingConfigurations = orig.mChangingConfigurations;
468                mDrawableState = orig.mDrawableState;
469                mInsetLeft = orig.mInsetLeft;
470                mInsetTop = orig.mInsetTop;
471                mInsetRight = orig.mInsetRight;
472                mInsetBottom = orig.mInsetBottom;
473            }
474        }
475
476        @Override
477        public boolean canApplyTheme() {
478            return mThemeAttrs != null
479                    || (mDrawableState != null && mDrawableState.canApplyTheme())
480                    || super.canApplyTheme();
481        }
482
483        @Override
484        public int addAtlasableBitmaps(Collection<Bitmap> atlasList) {
485            final ConstantState state = mDrawableState;
486            if (state != null) {
487                return state.addAtlasableBitmaps(atlasList);
488            }
489            return 0;
490        }
491
492        @Override
493        public Drawable newDrawable() {
494            return new InsetDrawable(this, null);
495        }
496
497        @Override
498        public Drawable newDrawable(Resources res) {
499            return new InsetDrawable(this, res);
500        }
501
502        @Override
503        public int getChangingConfigurations() {
504            return mChangingConfigurations;
505        }
506
507        public boolean canConstantState() {
508            return mDrawableState != null;
509        }
510    }
511
512    /**
513     * The one constructor to rule them all. This is called by all public
514     * constructors to set the state and initialize local properties.
515     */
516    private InsetDrawable(InsetState state, Resources res) {
517        mState = state;
518
519        updateLocalState(res);
520    }
521
522    /**
523     * Initializes local dynamic properties from state. This should be called
524     * after significant state changes, e.g. from the One True Constructor and
525     * after inflating or applying a theme.
526     */
527    private void updateLocalState(Resources res) {
528        if (mState.mDrawableState != null) {
529            final Drawable dr = mState.mDrawableState.newDrawable(res);
530            setDrawable(dr);
531        }
532    }
533}
534
535