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