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