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><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