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