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