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