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