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 
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.Resources.Theme;
28 import android.content.res.TypedArray;
29 import android.graphics.Bitmap;
30 import android.graphics.Insets;
31 import android.graphics.Outline;
32 import android.graphics.PixelFormat;
33 import android.graphics.Rect;
34 import android.util.AttributeSet;
35 import android.util.DisplayMetrics;
36 import android.util.TypedValue;
37 
38 import java.io.IOException;
39 
40 /**
41  * A Drawable that insets another Drawable by a specified distance or fraction of the content bounds.
42  * This is used when a View needs a background that is smaller than
43  * the View's actual bounds.
44  *
45  * <p>It can be defined in an XML file with the <code>&lt;inset></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#InsetDrawable_visible
50  * @attr ref android.R.styleable#InsetDrawable_drawable
51  * @attr ref android.R.styleable#InsetDrawable_insetLeft
52  * @attr ref android.R.styleable#InsetDrawable_insetRight
53  * @attr ref android.R.styleable#InsetDrawable_insetTop
54  * @attr ref android.R.styleable#InsetDrawable_insetBottom
55  */
56 public class InsetDrawable extends DrawableWrapper {
57     private final Rect mTmpRect = new Rect();
58     private final Rect mTmpInsetRect = new Rect();
59 
60     private InsetState mState;
61 
62     /**
63      * No-arg constructor used by drawable inflation.
64      */
InsetDrawable()65     InsetDrawable() {
66         this(new InsetState(null, null), null);
67     }
68 
69     /**
70      * Creates a new inset drawable with the specified inset.
71      *
72      * @param drawable The drawable to inset.
73      * @param inset Inset in pixels around the drawable.
74      */
InsetDrawable(@ullable Drawable drawable, int inset)75     public InsetDrawable(@Nullable Drawable drawable, int inset) {
76         this(drawable, inset, inset, inset, inset);
77     }
78 
79     /**
80      * Creates a new inset drawable with the specified inset.
81      *
82      * @param drawable The drawable to inset.
83      * @param inset Inset in fraction (range: [0, 1)) of the inset content bounds.
84      */
InsetDrawable(@ullable Drawable drawable, float inset)85     public InsetDrawable(@Nullable Drawable drawable, float inset) {
86         this(drawable, inset, inset, inset, inset);
87     }
88 
89     /**
90      * Creates a new inset drawable with the specified insets in pixels.
91      *
92      * @param drawable The drawable to inset.
93      * @param insetLeft Left inset in pixels.
94      * @param insetTop Top inset in pixels.
95      * @param insetRight Right inset in pixels.
96      * @param insetBottom Bottom inset in pixels.
97      */
InsetDrawable(@ullable Drawable drawable, int insetLeft, int insetTop, int insetRight, int insetBottom)98     public InsetDrawable(@Nullable Drawable drawable, int insetLeft, int insetTop,
99             int insetRight, int insetBottom) {
100         this(new InsetState(null, null), null);
101 
102         mState.mInsetLeft = new InsetValue(0f, insetLeft);
103         mState.mInsetTop = new InsetValue(0f, insetTop);
104         mState.mInsetRight = new InsetValue(0f, insetRight);
105         mState.mInsetBottom = new InsetValue(0f, insetBottom);
106 
107         setDrawable(drawable);
108     }
109 
110     /**
111      * Creates a new inset drawable with the specified insets in fraction of the view bounds.
112      *
113      * @param drawable The drawable to inset.
114      * @param insetLeftFraction Left inset in fraction (range: [0, 1)) of the inset content bounds.
115      * @param insetTopFraction Top inset in fraction (range: [0, 1)) of the inset content bounds.
116      * @param insetRightFraction Right inset in fraction (range: [0, 1)) of the inset content bounds.
117      * @param insetBottomFraction Bottom inset in fraction (range: [0, 1)) of the inset content bounds.
118      */
InsetDrawable(@ullable Drawable drawable, float insetLeftFraction, float insetTopFraction, float insetRightFraction, float insetBottomFraction)119     public InsetDrawable(@Nullable Drawable drawable, float insetLeftFraction,
120         float insetTopFraction, float insetRightFraction, float insetBottomFraction) {
121         this(new InsetState(null, null), null);
122 
123         mState.mInsetLeft = new InsetValue(insetLeftFraction, 0);
124         mState.mInsetTop = new InsetValue(insetTopFraction, 0);
125         mState.mInsetRight = new InsetValue(insetRightFraction, 0);
126         mState.mInsetBottom = new InsetValue(insetBottomFraction, 0);
127 
128         setDrawable(drawable);
129     }
130 
131     @Override
inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)132     public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser,
133             @NonNull AttributeSet attrs, @Nullable Theme theme)
134             throws XmlPullParserException, IOException {
135         final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.InsetDrawable);
136 
137         // Inflation will advance the XmlPullParser and AttributeSet.
138         super.inflate(r, parser, attrs, theme);
139 
140         updateStateFromTypedArray(a);
141         verifyRequiredAttributes(a);
142         a.recycle();
143     }
144 
145     @Override
applyTheme(@onNull Theme t)146     public void applyTheme(@NonNull Theme t) {
147         super.applyTheme(t);
148 
149         final InsetState state = mState;
150         if (state == null) {
151             return;
152         }
153 
154         if (state.mThemeAttrs != null) {
155             final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.InsetDrawable);
156             try {
157                 updateStateFromTypedArray(a);
158                 verifyRequiredAttributes(a);
159             } catch (XmlPullParserException e) {
160                 rethrowAsRuntimeException(e);
161             } finally {
162                 a.recycle();
163             }
164         }
165     }
166 
verifyRequiredAttributes(@onNull TypedArray a)167     private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException {
168         // If we're not waiting on a theme, verify required attributes.
169         if (getDrawable() == null && (mState.mThemeAttrs == null
170                 || mState.mThemeAttrs[R.styleable.InsetDrawable_drawable] == 0)) {
171             throw new XmlPullParserException(a.getPositionDescription()
172                     + ": <inset> tag requires a 'drawable' attribute or "
173                     + "child tag defining a drawable");
174         }
175     }
176 
updateStateFromTypedArray(@onNull TypedArray a)177     private void updateStateFromTypedArray(@NonNull TypedArray a) {
178         final InsetState state = mState;
179         if (state == null) {
180             return;
181         }
182 
183         // Account for any configuration changes.
184         state.mChangingConfigurations |= a.getChangingConfigurations();
185 
186         // Extract the theme attributes, if any.
187         state.mThemeAttrs = a.extractThemeAttrs();
188 
189         // Inset attribute may be overridden by more specific attributes.
190         if (a.hasValue(R.styleable.InsetDrawable_inset)) {
191             final InsetValue inset = getInset(a, R.styleable.InsetDrawable_inset, new InsetValue());
192             state.mInsetLeft = inset;
193             state.mInsetTop = inset;
194             state.mInsetRight = inset;
195             state.mInsetBottom = inset;
196         }
197         state.mInsetLeft = getInset(a, R.styleable.InsetDrawable_insetLeft, state.mInsetLeft);
198         state.mInsetTop = getInset(a, R.styleable.InsetDrawable_insetTop, state.mInsetTop);
199         state.mInsetRight = getInset(a, R.styleable.InsetDrawable_insetRight, state.mInsetRight);
200         state.mInsetBottom = getInset(a, R.styleable.InsetDrawable_insetBottom, state.mInsetBottom);
201     }
202 
getInset(@onNull TypedArray a, int index, InsetValue defaultValue)203     private InsetValue getInset(@NonNull TypedArray a, int index, InsetValue defaultValue) {
204         if (a.hasValue(index)) {
205             TypedValue tv = a.peekValue(index);
206             if (tv.type == TypedValue.TYPE_FRACTION) {
207                 float f = tv.getFraction(1.0f, 1.0f);
208                 if (f >= 1f) {
209                     throw new IllegalStateException("Fraction cannot be larger than 1");
210                 }
211                 return new InsetValue(f, 0);
212             } else {
213                 int dimension = a.getDimensionPixelOffset(index, 0);
214                 if (dimension != 0) {
215                     return new InsetValue(0, dimension);
216                 }
217             }
218         }
219         return defaultValue;
220     }
221 
getInsets(Rect out)222     private void getInsets(Rect out) {
223         final Rect b = getBounds();
224         out.left = mState.mInsetLeft.getDimension(b.width());
225         out.right = mState.mInsetRight.getDimension(b.width());
226         out.top = mState.mInsetTop.getDimension(b.height());
227         out.bottom = mState.mInsetBottom.getDimension(b.height());
228     }
229 
230     @Override
getPadding(Rect padding)231     public boolean getPadding(Rect padding) {
232         final boolean pad = super.getPadding(padding);
233         getInsets(mTmpInsetRect);
234         padding.left += mTmpInsetRect.left;
235         padding.right += mTmpInsetRect.right;
236         padding.top += mTmpInsetRect.top;
237         padding.bottom += mTmpInsetRect.bottom;
238 
239         return pad || (mTmpInsetRect.left | mTmpInsetRect.right
240                 | mTmpInsetRect.top | mTmpInsetRect.bottom) != 0;
241     }
242 
243     /** @hide */
244     @Override
getOpticalInsets()245     public Insets getOpticalInsets() {
246         final Insets contentInsets = super.getOpticalInsets();
247         getInsets(mTmpInsetRect);
248         return Insets.of(
249                 contentInsets.left + mTmpInsetRect.left,
250                 contentInsets.top + mTmpInsetRect.top,
251                 contentInsets.right + mTmpInsetRect.right,
252                 contentInsets.bottom + mTmpInsetRect.bottom);
253     }
254 
255     @Override
getOpacity()256     public int getOpacity() {
257         final InsetState state = mState;
258         final int opacity = getDrawable().getOpacity();
259         getInsets(mTmpInsetRect);
260         if (opacity == PixelFormat.OPAQUE &&
261             (mTmpInsetRect.left > 0 || mTmpInsetRect.top > 0 || mTmpInsetRect.right > 0
262                 || mTmpInsetRect.bottom > 0)) {
263             return PixelFormat.TRANSLUCENT;
264         }
265         return opacity;
266     }
267 
268     @Override
onBoundsChange(Rect bounds)269     protected void onBoundsChange(Rect bounds) {
270         final Rect r = mTmpRect;
271         r.set(bounds);
272 
273         r.left += mState.mInsetLeft.getDimension(bounds.width());
274         r.top += mState.mInsetTop.getDimension(bounds.height());
275         r.right -= mState.mInsetRight.getDimension(bounds.width());
276         r.bottom -= mState.mInsetBottom.getDimension(bounds.height());
277 
278         // Apply inset bounds to the wrapped drawable.
279         super.onBoundsChange(r);
280     }
281 
282     @Override
getIntrinsicWidth()283     public int getIntrinsicWidth() {
284         final int childWidth = getDrawable().getIntrinsicWidth();
285         final float fraction = mState.mInsetLeft.mFraction + mState.mInsetRight.mFraction;
286         if (childWidth < 0 || fraction >= 1) {
287             return -1;
288         }
289         return (int) (childWidth / (1 - fraction)) + mState.mInsetLeft.mDimension
290             + mState.mInsetRight.mDimension;
291     }
292 
293     @Override
getIntrinsicHeight()294     public int getIntrinsicHeight() {
295         final int childHeight = getDrawable().getIntrinsicHeight();
296         final float fraction = mState.mInsetTop.mFraction + mState.mInsetBottom.mFraction;
297         if (childHeight < 0 || fraction >= 1) {
298             return -1;
299         }
300         return (int) (childHeight / (1 - fraction)) + mState.mInsetTop.mDimension
301             + mState.mInsetBottom.mDimension;
302     }
303 
304     @Override
getOutline(@onNull Outline outline)305     public void getOutline(@NonNull Outline outline) {
306         getDrawable().getOutline(outline);
307     }
308 
309     @Override
mutateConstantState()310     DrawableWrapperState mutateConstantState() {
311         mState = new InsetState(mState, null);
312         return mState;
313     }
314 
315     static final class InsetState extends DrawableWrapper.DrawableWrapperState {
316         private int[] mThemeAttrs;
317 
318         InsetValue mInsetLeft;
319         InsetValue mInsetTop;
320         InsetValue mInsetRight;
321         InsetValue mInsetBottom;
322 
InsetState(@ullable InsetState orig, @Nullable Resources res)323         InsetState(@Nullable InsetState orig, @Nullable Resources res) {
324             super(orig, res);
325 
326             if (orig != null) {
327                 mInsetLeft = orig.mInsetLeft.clone();
328                 mInsetTop = orig.mInsetTop.clone();
329                 mInsetRight = orig.mInsetRight.clone();
330                 mInsetBottom = orig.mInsetBottom.clone();
331 
332                 if (orig.mDensity != mDensity) {
333                     applyDensityScaling(orig.mDensity, mDensity);
334                 }
335             } else {
336                 mInsetLeft = new InsetValue();
337                 mInsetTop = new InsetValue();
338                 mInsetRight = new InsetValue();
339                 mInsetBottom = new InsetValue();
340             }
341         }
342 
343         @Override
onDensityChanged(int sourceDensity, int targetDensity)344         void onDensityChanged(int sourceDensity, int targetDensity) {
345             super.onDensityChanged(sourceDensity, targetDensity);
346 
347             applyDensityScaling(sourceDensity, targetDensity);
348         }
349 
350         /**
351          * Called when the constant state density changes to scale
352          * density-dependent properties specific to insets.
353          *
354          * @param sourceDensity the previous constant state density
355          * @param targetDensity the new constant state density
356          */
applyDensityScaling(int sourceDensity, int targetDensity)357         private void applyDensityScaling(int sourceDensity, int targetDensity) {
358             mInsetLeft.scaleFromDensity(sourceDensity, targetDensity);
359             mInsetTop.scaleFromDensity(sourceDensity, targetDensity);
360             mInsetRight.scaleFromDensity(sourceDensity, targetDensity);
361             mInsetBottom.scaleFromDensity(sourceDensity, targetDensity);
362         }
363 
364         @Override
newDrawable(@ullable Resources res)365         public Drawable newDrawable(@Nullable Resources res) {
366             // If this drawable is being created for a different density,
367             // just create a new constant state and call it a day.
368             final InsetState state;
369             if (res != null) {
370                 final int densityDpi = res.getDisplayMetrics().densityDpi;
371                 final int density = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
372                 if (density != mDensity) {
373                     state = new InsetState(this, res);
374                 } else {
375                     state = this;
376                 }
377             } else {
378                 state = this;
379             }
380 
381             return new InsetDrawable(state, res);
382         }
383     }
384 
385     static final class InsetValue implements Cloneable {
386         final float mFraction;
387         int mDimension;
388 
InsetValue()389         public InsetValue() {
390             this(0f, 0);
391         }
392 
InsetValue(float fraction, int dimension)393         public InsetValue(float fraction, int dimension) {
394             mFraction = fraction;
395             mDimension = dimension;
396         }
getDimension(int boundSize)397         int getDimension(int boundSize) {
398             return (int) (boundSize * mFraction) + mDimension;
399         }
400 
scaleFromDensity(int sourceDensity, int targetDensity)401         void scaleFromDensity(int sourceDensity, int targetDensity) {
402             if (mDimension != 0) {
403                 mDimension = Bitmap.scaleFromDensity(mDimension, sourceDensity, targetDensity);
404             }
405         }
406 
407         @Override
clone()408         public InsetValue clone() {
409             return new InsetValue(mFraction, mDimension);
410         }
411     }
412 
413     /**
414      * The one constructor to rule them all. This is called by all public
415      * constructors to set the state and initialize local properties.
416      */
InsetDrawable(@onNull InsetState state, @Nullable Resources res)417     private InsetDrawable(@NonNull InsetState state, @Nullable Resources res) {
418         super(state, res);
419 
420         mState = state;
421     }
422 }
423 
424