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