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 17package android.graphics.drawable; 18 19import com.android.internal.R; 20 21import org.xmlpull.v1.XmlPullParser; 22import org.xmlpull.v1.XmlPullParserException; 23 24import android.annotation.NonNull; 25import android.annotation.Nullable; 26import android.content.pm.ActivityInfo.Config; 27import android.content.res.ColorStateList; 28import android.content.res.Resources; 29import android.content.res.Resources.Theme; 30import android.content.res.TypedArray; 31import android.graphics.Canvas; 32import android.graphics.ColorFilter; 33import android.graphics.Insets; 34import android.graphics.Outline; 35import android.graphics.PixelFormat; 36import android.graphics.PorterDuff; 37import android.graphics.Rect; 38import android.util.AttributeSet; 39import android.util.DisplayMetrics; 40import android.view.View; 41 42import java.io.IOException; 43 44/** 45 * Drawable container with only one child element. 46 */ 47public 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 void setTintList(@Nullable ColorStateList tint) { 300 if (mDrawable != null) { 301 mDrawable.setTintList(tint); 302 } 303 } 304 305 @Override 306 public void setTintMode(@Nullable PorterDuff.Mode tintMode) { 307 if (mDrawable != null) { 308 mDrawable.setTintMode(tintMode); 309 } 310 } 311 312 @Override 313 public boolean onLayoutDirectionChanged(@View.ResolvedLayoutDir int layoutDirection) { 314 return mDrawable != null && mDrawable.setLayoutDirection(layoutDirection); 315 } 316 317 @Override 318 public int getOpacity() { 319 return mDrawable != null ? mDrawable.getOpacity() : PixelFormat.TRANSPARENT; 320 } 321 322 @Override 323 public boolean isStateful() { 324 return mDrawable != null && mDrawable.isStateful(); 325 } 326 327 /** @hide */ 328 @Override 329 public boolean hasFocusStateSpecified() { 330 return mDrawable != null && mDrawable.hasFocusStateSpecified(); 331 } 332 333 @Override 334 protected boolean onStateChange(int[] state) { 335 if (mDrawable != null && mDrawable.isStateful()) { 336 final boolean changed = mDrawable.setState(state); 337 if (changed) { 338 onBoundsChange(getBounds()); 339 } 340 return changed; 341 } 342 return false; 343 } 344 345 @Override 346 protected boolean onLevelChange(int level) { 347 return mDrawable != null && mDrawable.setLevel(level); 348 } 349 350 @Override 351 protected void onBoundsChange(@NonNull Rect bounds) { 352 if (mDrawable != null) { 353 mDrawable.setBounds(bounds); 354 } 355 } 356 357 @Override 358 public int getIntrinsicWidth() { 359 return mDrawable != null ? mDrawable.getIntrinsicWidth() : -1; 360 } 361 362 @Override 363 public int getIntrinsicHeight() { 364 return mDrawable != null ? mDrawable.getIntrinsicHeight() : -1; 365 } 366 367 @Override 368 public void getOutline(@NonNull Outline outline) { 369 if (mDrawable != null) { 370 mDrawable.getOutline(outline); 371 } else { 372 super.getOutline(outline); 373 } 374 } 375 376 @Override 377 @Nullable 378 public ConstantState getConstantState() { 379 if (mState != null && mState.canConstantState()) { 380 mState.mChangingConfigurations = getChangingConfigurations(); 381 return mState; 382 } 383 return null; 384 } 385 386 @Override 387 @NonNull 388 public Drawable mutate() { 389 if (!mMutated && super.mutate() == this) { 390 mState = mutateConstantState(); 391 if (mDrawable != null) { 392 mDrawable.mutate(); 393 } 394 if (mState != null) { 395 mState.mDrawableState = mDrawable != null ? mDrawable.getConstantState() : null; 396 } 397 mMutated = true; 398 } 399 return this; 400 } 401 402 /** 403 * Mutates the constant state and returns the new state. Responsible for 404 * updating any local copy. 405 * <p> 406 * This method should never call the super implementation; it should always 407 * mutate and return its own constant state. 408 * 409 * @return the new state 410 */ 411 DrawableWrapperState mutateConstantState() { 412 return mState; 413 } 414 415 /** 416 * @hide Only used by the framework for pre-loading resources. 417 */ 418 public void clearMutated() { 419 super.clearMutated(); 420 if (mDrawable != null) { 421 mDrawable.clearMutated(); 422 } 423 mMutated = false; 424 } 425 426 /** 427 * Called during inflation to inflate the child element. The last valid 428 * child element will take precedence over any other child elements or 429 * explicit drawable attribute. 430 */ 431 private void inflateChildDrawable(@NonNull Resources r, @NonNull XmlPullParser parser, 432 @NonNull AttributeSet attrs, @Nullable Theme theme) 433 throws XmlPullParserException, IOException { 434 // Seek to the first child element. 435 Drawable dr = null; 436 int type; 437 final int outerDepth = parser.getDepth(); 438 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 439 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 440 if (type == XmlPullParser.START_TAG) { 441 dr = Drawable.createFromXmlInnerForDensity(r, parser, attrs, 442 mState.mSrcDensityOverride, theme); 443 } 444 } 445 446 if (dr != null) { 447 setDrawable(dr); 448 } 449 } 450 451 abstract static class DrawableWrapperState extends Drawable.ConstantState { 452 private int[] mThemeAttrs; 453 454 @Config int mChangingConfigurations; 455 int mDensity = DisplayMetrics.DENSITY_DEFAULT; 456 457 /** 458 * The density to use when looking up resources from 459 * {@link Resources#getDrawableForDensity(int, int, Theme)}. 460 * A value of 0 means there is no override and the system density will be used. 461 * @hide 462 */ 463 int mSrcDensityOverride = 0; 464 465 Drawable.ConstantState mDrawableState; 466 467 DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) { 468 if (orig != null) { 469 mThemeAttrs = orig.mThemeAttrs; 470 mChangingConfigurations = orig.mChangingConfigurations; 471 mDrawableState = orig.mDrawableState; 472 mSrcDensityOverride = orig.mSrcDensityOverride; 473 } 474 475 final int density; 476 if (res != null) { 477 density = res.getDisplayMetrics().densityDpi; 478 } else if (orig != null) { 479 density = orig.mDensity; 480 } else { 481 density = 0; 482 } 483 484 mDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; 485 } 486 487 /** 488 * Sets the constant state density. 489 * <p> 490 * If the density has been previously set, dispatches the change to 491 * subclasses so that density-dependent properties may be scaled as 492 * necessary. 493 * 494 * @param targetDensity the new constant state density 495 */ 496 public final void setDensity(int targetDensity) { 497 if (mDensity != targetDensity) { 498 final int sourceDensity = mDensity; 499 mDensity = targetDensity; 500 501 onDensityChanged(sourceDensity, targetDensity); 502 } 503 } 504 505 /** 506 * Called when the constant state density changes. 507 * <p> 508 * Subclasses with density-dependent constant state properties should 509 * override this method and scale their properties as necessary. 510 * 511 * @param sourceDensity the previous constant state density 512 * @param targetDensity the new constant state density 513 */ 514 void onDensityChanged(int sourceDensity, int targetDensity) { 515 // Stub method. 516 } 517 518 @Override 519 public boolean canApplyTheme() { 520 return mThemeAttrs != null 521 || (mDrawableState != null && mDrawableState.canApplyTheme()) 522 || super.canApplyTheme(); 523 } 524 525 @Override 526 public Drawable newDrawable() { 527 return newDrawable(null); 528 } 529 530 @Override 531 public abstract Drawable newDrawable(@Nullable Resources res); 532 533 @Override 534 public @Config int getChangingConfigurations() { 535 return mChangingConfigurations 536 | (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0); 537 } 538 539 public boolean canConstantState() { 540 return mDrawableState != null; 541 } 542 } 543} 544