1 /* 2 * Copyright (C) 2015 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.pm.ActivityInfo.Config; 23 import android.content.res.ColorStateList; 24 import android.content.res.Resources; 25 import android.content.res.Resources.Theme; 26 import android.content.res.TypedArray; 27 import android.graphics.BlendMode; 28 import android.graphics.Canvas; 29 import android.graphics.ColorFilter; 30 import android.graphics.Insets; 31 import android.graphics.Outline; 32 import android.graphics.PixelFormat; 33 import android.graphics.Rect; 34 import android.graphics.Xfermode; 35 import android.os.Build; 36 import android.util.AttributeSet; 37 import android.util.DisplayMetrics; 38 import android.view.View; 39 40 import com.android.internal.R; 41 42 import org.xmlpull.v1.XmlPullParser; 43 import org.xmlpull.v1.XmlPullParserException; 44 45 import java.io.IOException; 46 47 /** 48 * Drawable container with only one child element. 49 */ 50 public abstract class DrawableWrapper extends Drawable implements Drawable.Callback { 51 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 52 private DrawableWrapperState mState; 53 private Drawable mDrawable; 54 private boolean mMutated; 55 DrawableWrapper(DrawableWrapperState state, Resources res)56 DrawableWrapper(DrawableWrapperState state, Resources res) { 57 mState = state; 58 59 updateLocalState(res); 60 } 61 62 /** 63 * Creates a new wrapper around the specified drawable. 64 * 65 * @param dr the drawable to wrap 66 */ DrawableWrapper(@ullable Drawable dr)67 public DrawableWrapper(@Nullable Drawable dr) { 68 mState = null; 69 setDrawable(dr); 70 } 71 72 /** 73 * Initializes local dynamic properties from state. This should be called 74 * after significant state changes, e.g. from the One True Constructor and 75 * after inflating or applying a theme. 76 */ updateLocalState(Resources res)77 private void updateLocalState(Resources res) { 78 if (mState != null && mState.mDrawableState != null) { 79 final Drawable dr = mState.mDrawableState.newDrawable(res); 80 setDrawable(dr); 81 } 82 } 83 84 /** 85 * @hide 86 */ 87 @Override setXfermode(Xfermode mode)88 public void setXfermode(Xfermode mode) { 89 if (mDrawable != null) { 90 mDrawable.setXfermode(mode); 91 } 92 } 93 94 /** 95 * Sets the wrapped drawable. 96 * 97 * @param dr the wrapped drawable 98 */ setDrawable(@ullable Drawable dr)99 public void setDrawable(@Nullable Drawable dr) { 100 if (mDrawable != null) { 101 mDrawable.setCallback(null); 102 } 103 104 mDrawable = dr; 105 106 if (dr != null) { 107 dr.setCallback(this); 108 109 // Only call setters for data that's stored in the base Drawable. 110 dr.setVisible(isVisible(), true); 111 dr.setState(getState()); 112 dr.setLevel(getLevel()); 113 dr.setBounds(getBounds()); 114 dr.setLayoutDirection(getLayoutDirection()); 115 116 if (mState != null) { 117 mState.mDrawableState = dr.getConstantState(); 118 } 119 } 120 121 invalidateSelf(); 122 } 123 124 /** 125 * @return the wrapped drawable 126 */ 127 @Nullable getDrawable()128 public Drawable getDrawable() { 129 return mDrawable; 130 } 131 132 @Override inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)133 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 134 @NonNull AttributeSet attrs, @Nullable Theme theme) 135 throws XmlPullParserException, IOException { 136 super.inflate(r, parser, attrs, theme); 137 138 final DrawableWrapperState state = mState; 139 if (state == null) { 140 return; 141 } 142 143 // The density may have changed since the last update. This will 144 // apply scaling to any existing constant state properties. 145 final int densityDpi = r.getDisplayMetrics().densityDpi; 146 final int targetDensity = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi; 147 state.setDensity(targetDensity); 148 state.mSrcDensityOverride = mSrcDensityOverride; 149 150 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.DrawableWrapper); 151 updateStateFromTypedArray(a); 152 a.recycle(); 153 154 inflateChildDrawable(r, parser, attrs, theme); 155 } 156 157 @Override applyTheme(@onNull Theme t)158 public void applyTheme(@NonNull Theme t) { 159 super.applyTheme(t); 160 161 // If we load the drawable later as part of updating from the typed 162 // array, it will already be themed correctly. So, we can theme the 163 // local drawable first. 164 if (mDrawable != null && mDrawable.canApplyTheme()) { 165 mDrawable.applyTheme(t); 166 } 167 168 final DrawableWrapperState state = mState; 169 if (state == null) { 170 return; 171 } 172 173 final int densityDpi = t.getResources().getDisplayMetrics().densityDpi; 174 final int density = densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi; 175 state.setDensity(density); 176 177 if (state.mThemeAttrs != null) { 178 final TypedArray a = t.resolveAttributes( 179 state.mThemeAttrs, R.styleable.DrawableWrapper); 180 updateStateFromTypedArray(a); 181 a.recycle(); 182 } 183 } 184 185 /** 186 * Updates constant state properties from the provided typed array. 187 * <p> 188 * Implementing subclasses should call through to the super method first. 189 * 190 * @param a the typed array rom which properties should be read 191 */ updateStateFromTypedArray(@onNull TypedArray a)192 private void updateStateFromTypedArray(@NonNull TypedArray a) { 193 final DrawableWrapperState state = mState; 194 if (state == null) { 195 return; 196 } 197 198 // Account for any configuration changes. 199 state.mChangingConfigurations |= a.getChangingConfigurations(); 200 201 // Extract the theme attributes, if any. 202 state.mThemeAttrs = a.extractThemeAttrs(); 203 204 if (a.hasValueOrEmpty(R.styleable.DrawableWrapper_drawable)) { 205 setDrawable(a.getDrawable(R.styleable.DrawableWrapper_drawable)); 206 } 207 } 208 209 @Override canApplyTheme()210 public boolean canApplyTheme() { 211 return (mState != null && mState.canApplyTheme()) || super.canApplyTheme(); 212 } 213 214 @Override invalidateDrawable(@onNull Drawable who)215 public void invalidateDrawable(@NonNull Drawable who) { 216 final Callback callback = getCallback(); 217 if (callback != null) { 218 callback.invalidateDrawable(this); 219 } 220 } 221 222 @Override scheduleDrawable(@onNull Drawable who, @NonNull Runnable what, long when)223 public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) { 224 final Callback callback = getCallback(); 225 if (callback != null) { 226 callback.scheduleDrawable(this, what, when); 227 } 228 } 229 230 @Override unscheduleDrawable(@onNull Drawable who, @NonNull Runnable what)231 public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { 232 final Callback callback = getCallback(); 233 if (callback != null) { 234 callback.unscheduleDrawable(this, what); 235 } 236 } 237 238 @Override draw(@onNull Canvas canvas)239 public void draw(@NonNull Canvas canvas) { 240 if (mDrawable != null) { 241 mDrawable.draw(canvas); 242 } 243 } 244 245 @Override getChangingConfigurations()246 public @Config int getChangingConfigurations() { 247 return super.getChangingConfigurations() 248 | (mState != null ? mState.getChangingConfigurations() : 0) 249 | mDrawable.getChangingConfigurations(); 250 } 251 252 @Override getPadding(@onNull Rect padding)253 public boolean getPadding(@NonNull Rect padding) { 254 return mDrawable != null && mDrawable.getPadding(padding); 255 } 256 257 @Override getOpticalInsets()258 public Insets getOpticalInsets() { 259 return mDrawable != null ? mDrawable.getOpticalInsets() : Insets.NONE; 260 } 261 262 @Override setHotspot(float x, float y)263 public void setHotspot(float x, float y) { 264 if (mDrawable != null) { 265 mDrawable.setHotspot(x, y); 266 } 267 } 268 269 @Override setHotspotBounds(int left, int top, int right, int bottom)270 public void setHotspotBounds(int left, int top, int right, int bottom) { 271 if (mDrawable != null) { 272 mDrawable.setHotspotBounds(left, top, right, bottom); 273 } 274 } 275 276 @Override getHotspotBounds(@onNull Rect outRect)277 public void getHotspotBounds(@NonNull Rect outRect) { 278 if (mDrawable != null) { 279 mDrawable.getHotspotBounds(outRect); 280 } else { 281 outRect.set(getBounds()); 282 } 283 } 284 285 @Override setVisible(boolean visible, boolean restart)286 public boolean setVisible(boolean visible, boolean restart) { 287 final boolean superChanged = super.setVisible(visible, restart); 288 final boolean changed = mDrawable != null && mDrawable.setVisible(visible, restart); 289 return superChanged | changed; 290 } 291 292 @Override setAlpha(int alpha)293 public void setAlpha(int alpha) { 294 if (mDrawable != null) { 295 mDrawable.setAlpha(alpha); 296 } 297 } 298 299 @Override getAlpha()300 public int getAlpha() { 301 return mDrawable != null ? mDrawable.getAlpha() : 255; 302 } 303 304 @Override setColorFilter(@ullable ColorFilter colorFilter)305 public void setColorFilter(@Nullable ColorFilter colorFilter) { 306 if (mDrawable != null) { 307 mDrawable.setColorFilter(colorFilter); 308 } 309 } 310 311 @Override getColorFilter()312 public ColorFilter getColorFilter() { 313 final Drawable drawable = getDrawable(); 314 if (drawable != null) { 315 return drawable.getColorFilter(); 316 } 317 return super.getColorFilter(); 318 } 319 320 @Override setTintList(@ullable ColorStateList tint)321 public void setTintList(@Nullable ColorStateList tint) { 322 if (mDrawable != null) { 323 mDrawable.setTintList(tint); 324 } 325 } 326 327 @Override setTintBlendMode(@onNull BlendMode blendMode)328 public void setTintBlendMode(@NonNull BlendMode blendMode) { 329 if (mDrawable != null) { 330 mDrawable.setTintBlendMode(blendMode); 331 } 332 } 333 334 @Override onLayoutDirectionChanged(@iew.ResolvedLayoutDir int layoutDirection)335 public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) { 336 return mDrawable != null && mDrawable.setLayoutDirection(layoutDirection); 337 } 338 339 @Override getOpacity()340 public int getOpacity() { 341 return mDrawable != null ? mDrawable.getOpacity() : PixelFormat.TRANSPARENT; 342 } 343 344 @Override isStateful()345 public boolean isStateful() { 346 return mDrawable != null && mDrawable.isStateful(); 347 } 348 349 @Override hasFocusStateSpecified()350 public boolean hasFocusStateSpecified() { 351 return mDrawable != null && mDrawable.hasFocusStateSpecified(); 352 } 353 354 @Override onStateChange(@onNull int[] state)355 protected boolean onStateChange(@NonNull int[] state) { 356 if (mDrawable != null && mDrawable.isStateful()) { 357 final boolean changed = mDrawable.setState(state); 358 if (changed) { 359 onBoundsChange(getBounds()); 360 } 361 return changed; 362 } 363 return false; 364 } 365 366 @Override jumpToCurrentState()367 public void jumpToCurrentState() { 368 if (mDrawable != null) { 369 mDrawable.jumpToCurrentState(); 370 } 371 } 372 373 @Override onLevelChange(int level)374 protected boolean onLevelChange(int level) { 375 return mDrawable != null && mDrawable.setLevel(level); 376 } 377 378 @Override onBoundsChange(@onNull Rect bounds)379 protected void onBoundsChange(@NonNull Rect bounds) { 380 if (mDrawable != null) { 381 mDrawable.setBounds(bounds); 382 } 383 } 384 385 @Override getIntrinsicWidth()386 public int getIntrinsicWidth() { 387 return mDrawable != null ? mDrawable.getIntrinsicWidth() : -1; 388 } 389 390 @Override getIntrinsicHeight()391 public int getIntrinsicHeight() { 392 return mDrawable != null ? mDrawable.getIntrinsicHeight() : -1; 393 } 394 395 @Override getOutline(@onNull Outline outline)396 public void getOutline(@NonNull Outline outline) { 397 if (mDrawable != null) { 398 mDrawable.getOutline(outline); 399 } else { 400 super.getOutline(outline); 401 } 402 } 403 404 @Override 405 @Nullable getConstantState()406 public ConstantState getConstantState() { 407 if (mState != null && mState.canConstantState()) { 408 mState.mChangingConfigurations = getChangingConfigurations(); 409 return mState; 410 } 411 return null; 412 } 413 414 @Override 415 @NonNull mutate()416 public Drawable mutate() { 417 if (!mMutated && super.mutate() == this) { 418 mState = mutateConstantState(); 419 if (mDrawable != null) { 420 mDrawable.mutate(); 421 } 422 if (mState != null) { 423 mState.mDrawableState = mDrawable != null ? mDrawable.getConstantState() : null; 424 } 425 mMutated = true; 426 } 427 return this; 428 } 429 430 /** 431 * Mutates the constant state and returns the new state. Responsible for 432 * updating any local copy. 433 * <p> 434 * This method should never call the super implementation; it should always 435 * mutate and return its own constant state. 436 * 437 * @return the new state 438 */ mutateConstantState()439 DrawableWrapperState mutateConstantState() { 440 return mState; 441 } 442 443 /** 444 * @hide Only used by the framework for pre-loading resources. 445 */ clearMutated()446 public void clearMutated() { 447 super.clearMutated(); 448 if (mDrawable != null) { 449 mDrawable.clearMutated(); 450 } 451 mMutated = false; 452 } 453 454 /** 455 * Called during inflation to inflate the child element. The last valid 456 * child element will take precedence over any other child elements or 457 * explicit drawable attribute. 458 */ inflateChildDrawable(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)459 private void inflateChildDrawable(@NonNull Resources r, @NonNull XmlPullParser parser, 460 @NonNull AttributeSet attrs, @Nullable Theme theme) 461 throws XmlPullParserException, IOException { 462 // Seek to the first child element. 463 Drawable dr = null; 464 int type; 465 final int outerDepth = parser.getDepth(); 466 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 467 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 468 if (type == XmlPullParser.START_TAG) { 469 dr = Drawable.createFromXmlInnerForDensity(r, parser, attrs, 470 mState.mSrcDensityOverride, theme); 471 } 472 } 473 474 if (dr != null) { 475 setDrawable(dr); 476 } 477 } 478 479 abstract static class DrawableWrapperState extends Drawable.ConstantState { 480 private int[] mThemeAttrs; 481 482 @Config int mChangingConfigurations; 483 int mDensity = DisplayMetrics.DENSITY_DEFAULT; 484 485 /** 486 * The density to use when looking up resources from 487 * {@link Resources#getDrawableForDensity(int, int, Theme)}. 488 * A value of 0 means there is no override and the system density will be used. 489 * @hide 490 */ 491 int mSrcDensityOverride = 0; 492 493 Drawable.ConstantState mDrawableState; 494 DrawableWrapperState(@ullable DrawableWrapperState orig, @Nullable Resources res)495 DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) { 496 if (orig != null) { 497 mThemeAttrs = orig.mThemeAttrs; 498 mChangingConfigurations = orig.mChangingConfigurations; 499 mDrawableState = orig.mDrawableState; 500 mSrcDensityOverride = orig.mSrcDensityOverride; 501 } 502 503 final int density; 504 if (res != null) { 505 density = res.getDisplayMetrics().densityDpi; 506 } else if (orig != null) { 507 density = orig.mDensity; 508 } else { 509 density = 0; 510 } 511 512 mDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; 513 } 514 515 /** 516 * Sets the constant state density. 517 * <p> 518 * If the density has been previously set, dispatches the change to 519 * subclasses so that density-dependent properties may be scaled as 520 * necessary. 521 * 522 * @param targetDensity the new constant state density 523 */ setDensity(int targetDensity)524 public final void setDensity(int targetDensity) { 525 if (mDensity != targetDensity) { 526 final int sourceDensity = mDensity; 527 mDensity = targetDensity; 528 529 onDensityChanged(sourceDensity, targetDensity); 530 } 531 } 532 533 /** 534 * Called when the constant state density changes. 535 * <p> 536 * Subclasses with density-dependent constant state properties should 537 * override this method and scale their properties as necessary. 538 * 539 * @param sourceDensity the previous constant state density 540 * @param targetDensity the new constant state density 541 */ onDensityChanged(int sourceDensity, int targetDensity)542 void onDensityChanged(int sourceDensity, int targetDensity) { 543 // Stub method. 544 } 545 546 @Override canApplyTheme()547 public boolean canApplyTheme() { 548 return mThemeAttrs != null 549 || (mDrawableState != null && mDrawableState.canApplyTheme()) 550 || super.canApplyTheme(); 551 } 552 553 @Override newDrawable()554 public Drawable newDrawable() { 555 return newDrawable(null); 556 } 557 558 @Override newDrawable(@ullable Resources res)559 public abstract Drawable newDrawable(@Nullable Resources res); 560 561 @Override getChangingConfigurations()562 public @Config int getChangingConfigurations() { 563 return mChangingConfigurations 564 | (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0); 565 } 566 canConstantState()567 public boolean canConstantState() { 568 return mDrawableState != null; 569 } 570 } 571 } 572