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