RippleDrawable.java revision e3c433aa457138425e514494e4d06590076a1d07
1/* 2 * Copyright (C) 2013 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 android.content.res.ColorStateList; 20import android.content.res.Resources; 21import android.content.res.Resources.Theme; 22import android.content.res.TypedArray; 23import android.graphics.Canvas; 24import android.graphics.Color; 25import android.graphics.ColorFilter; 26import android.graphics.Paint; 27import android.graphics.PixelFormat; 28import android.graphics.PorterDuff.Mode; 29import android.graphics.PorterDuffXfermode; 30import android.graphics.Rect; 31import android.util.AttributeSet; 32import android.util.DisplayMetrics; 33import android.util.Log; 34 35import com.android.internal.R; 36 37import org.xmlpull.v1.XmlPullParser; 38import org.xmlpull.v1.XmlPullParserException; 39 40import java.io.IOException; 41 42/** 43 * Drawable that shows a ripple effect in response to state changes. The 44 * anchoring position of the ripple for a given state may be specified by 45 * calling {@link #setHotspot(float, float)} with the corresponding state 46 * attribute identifier. 47 * <p> 48 * A touch feedback drawable may contain multiple child layers, including a 49 * special mask layer that is not drawn to the screen. A single layer may be set 50 * as the mask by specifying its android:id value as {@link android.R.id#mask}. 51 * <pre> 52 * <code><!-- A red ripple masked against an opaque rectangle. --/> 53 * <ripple android:color="#ffff0000"> 54 * <item android:id="@android:id/mask" 55 * android:drawable="#ffffffff" /> 56 * <ripple /></code> 57 * </pre> 58 * <p> 59 * If a mask layer is set, the ripple effect will be masked against that layer 60 * before it is drawn over the composite of the remaining child layers. 61 * <p> 62 * If no mask layer is set, the ripple effect is masked against the composite 63 * of the child layers. 64 * <pre> 65 * <code><!-- A blue ripple drawn atop a green rectangle. --/> 66 * <ripple android:color="#ff00ff00"> 67 * <item android:drawable="#ff0000ff" /> 68 * <ripple /> 69 * 70 * <!-- A red ripple drawn atop a drawable resource. --/> 71 * <ripple android:color="#ff00ff00"> 72 * <item android:drawable="@drawable/my_drawable" /> 73 * <ripple /></code> 74 * </pre> 75 * <p> 76 * If no child layers or mask is specified and the ripple is set as a View 77 * background, the ripple will be drawn atop the first available parent 78 * background within the View's hierarchy. In this case, the drawing region 79 * may extend outside of the Drawable bounds. 80 * <pre> 81 * <code><!-- An unbounded green ripple. --/> 82 * <ripple android:color="#ff0000ff" /></code> 83 * </pre> 84 * 85 * @attr ref android.R.styleable#RippleDrawable_color 86 */ 87public class RippleDrawable extends LayerDrawable { 88 private static final String LOG_TAG = RippleDrawable.class.getSimpleName(); 89 private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN); 90 private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP); 91 private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER); 92 93 /** 94 * Constant for automatically determining the maximum ripple radius. 95 * 96 * @see #setMaxRadius(int) 97 * @hide 98 */ 99 public static final int RADIUS_AUTO = -1; 100 101 /** The maximum number of ripples supported. */ 102 private static final int MAX_RIPPLES = 10; 103 104 private final Rect mTempRect = new Rect(); 105 106 /** Current ripple effect bounds, used to constrain ripple effects. */ 107 private final Rect mHotspotBounds = new Rect(); 108 109 /** Current drawing bounds, used to compute dirty region. */ 110 private final Rect mDrawingBounds = new Rect(); 111 112 /** Current dirty bounds, union of current and previous drawing bounds. */ 113 private final Rect mDirtyBounds = new Rect(); 114 115 private final RippleState mState; 116 117 /** The masking layer, e.g. the layer with id R.id.mask. */ 118 private Drawable mMask; 119 120 /** The current hotspot. May be actively animating or pending entry. */ 121 private Ripple mHotspot; 122 123 /** 124 * Lazily-created array of actively animating ripples. Inactive ripples are 125 * pruned during draw(). The locations of these will not change. 126 */ 127 private Ripple[] mAnimatingRipples; 128 private int mAnimatingRipplesCount = 0; 129 130 /** Paint used to control appearance of ripples. */ 131 private Paint mRipplePaint; 132 133 /** Paint used to control reveal layer masking. */ 134 private Paint mMaskingPaint; 135 136 /** Target density of the display into which ripples are drawn. */ 137 private float mDensity = 1.0f; 138 139 /** Whether bounds are being overridden. */ 140 private boolean mOverrideBounds; 141 142 /** Whether the hotspot is currently active (e.g. focused or pressed). */ 143 private boolean mActive; 144 145 RippleDrawable() { 146 this(null, null); 147 } 148 149 /** 150 * Creates a new ripple drawable with the specified content and mask 151 * drawables. 152 * 153 * @param content The content drawable, may be {@code null} 154 * @param mask The mask drawable, may be {@code null} 155 */ 156 public RippleDrawable(Drawable content, Drawable mask) { 157 this(new RippleState(null, null, null), null, null); 158 159 if (content != null) { 160 addLayer(content, null, 0, 0, 0, 0, 0); 161 } 162 163 if (mask != null) { 164 addLayer(content, null, android.R.id.mask, 0, 0, 0, 0); 165 } 166 167 ensurePadding(); 168 } 169 170 @Override 171 public void setAlpha(int alpha) { 172 super.setAlpha(alpha); 173 174 // TODO: Should we support this? 175 } 176 177 @Override 178 public void setColorFilter(ColorFilter cf) { 179 super.setColorFilter(cf); 180 181 // TODO: Should we support this? 182 } 183 184 @Override 185 public int getOpacity() { 186 // Worst-case scenario. 187 return PixelFormat.TRANSLUCENT; 188 } 189 190 @Override 191 protected boolean onStateChange(int[] stateSet) { 192 super.onStateChange(stateSet); 193 194 // TODO: This would make more sense in a StateListDrawable. 195 boolean active = false; 196 boolean enabled = false; 197 final int N = stateSet.length; 198 for (int i = 0; i < N; i++) { 199 if (stateSet[i] == R.attr.state_enabled) { 200 enabled = true; 201 } 202 if (stateSet[i] == R.attr.state_focused 203 || stateSet[i] == R.attr.state_pressed) { 204 active = true; 205 } 206 } 207 setActive(active && enabled); 208 209 // Update the paint color. Only applicable when animated in software. 210 if (mRipplePaint != null && mState.mColor != null) { 211 final ColorStateList stateList = mState.mColor; 212 final int newColor = stateList.getColorForState(stateSet, 0); 213 final int oldColor = mRipplePaint.getColor(); 214 if (oldColor != newColor) { 215 mRipplePaint.setColor(newColor); 216 invalidateSelf(); 217 return true; 218 } 219 } 220 221 return false; 222 } 223 224 private void setActive(boolean active) { 225 if (mActive != active) { 226 mActive = active; 227 228 if (active) { 229 activateHotspot(); 230 } else { 231 removeHotspot(); 232 } 233 } 234 } 235 236 @Override 237 protected void onBoundsChange(Rect bounds) { 238 super.onBoundsChange(bounds); 239 240 if (!mOverrideBounds) { 241 mHotspotBounds.set(bounds); 242 onHotspotBoundsChanged(); 243 } 244 245 invalidateSelf(); 246 } 247 248 @Override 249 public boolean setVisible(boolean visible, boolean restart) { 250 if (!visible) { 251 clearHotspots(); 252 } 253 254 return super.setVisible(visible, restart); 255 } 256 257 /** 258 * @hide 259 */ 260 @Override 261 public boolean isProjected() { 262 return getNumberOfLayers() == 0; 263 } 264 265 @Override 266 public boolean isStateful() { 267 return true; 268 } 269 270 public void setColor(ColorStateList color) { 271 mState.mColor = color; 272 invalidateSelf(); 273 } 274 275 @Override 276 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 277 throws XmlPullParserException, IOException { 278 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); 279 updateStateFromTypedArray(a); 280 a.recycle(); 281 282 // Force padding default to STACK before inflating. 283 setPaddingMode(PADDING_MODE_STACK); 284 285 super.inflate(r, parser, attrs, theme); 286 287 setTargetDensity(r.getDisplayMetrics()); 288 initializeFromState(); 289 } 290 291 @Override 292 public boolean setDrawableByLayerId(int id, Drawable drawable) { 293 if (super.setDrawableByLayerId(id, drawable)) { 294 if (id == R.id.mask) { 295 mMask = drawable; 296 } 297 298 return true; 299 } 300 301 return false; 302 } 303 304 /** 305 * Specifies how layer padding should affect the bounds of subsequent 306 * layers. The default and recommended value for RippleDrawable is 307 * {@link #PADDING_MODE_STACK}. 308 * 309 * @param mode padding mode, one of: 310 * <ul> 311 * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the 312 * padding of the previous layer 313 * <li>{@link #PADDING_MODE_STACK} to stack each layer directly 314 * atop the previous layer 315 * </ul> 316 * @see #getPaddingMode() 317 */ 318 @Override 319 public void setPaddingMode(int mode) { 320 super.setPaddingMode(mode); 321 } 322 323 /** 324 * Initializes the constant state from the values in the typed array. 325 */ 326 private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { 327 final RippleState state = mState; 328 329 // Extract the theme attributes, if any. 330 state.mTouchThemeAttrs = a.extractThemeAttrs(); 331 332 final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color); 333 if (color != null) { 334 mState.mColor = color; 335 } 336 337 // If we're not waiting on a theme, verify required attributes. 338 if (state.mTouchThemeAttrs == null && mState.mColor == null) { 339 throw new XmlPullParserException(a.getPositionDescription() + 340 ": <ripple> requires a valid color attribute"); 341 } 342 } 343 344 /** 345 * Set the density at which this drawable will be rendered. 346 * 347 * @param metrics The display metrics for this drawable. 348 */ 349 private void setTargetDensity(DisplayMetrics metrics) { 350 if (mDensity != metrics.density) { 351 mDensity = metrics.density; 352 invalidateSelf(); 353 } 354 } 355 356 @Override 357 public void applyTheme(Theme t) { 358 super.applyTheme(t); 359 360 final RippleState state = mState; 361 if (state == null || state.mTouchThemeAttrs == null) { 362 return; 363 } 364 365 final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, 366 R.styleable.RippleDrawable); 367 try { 368 updateStateFromTypedArray(a); 369 } catch (XmlPullParserException e) { 370 throw new RuntimeException(e); 371 } finally { 372 a.recycle(); 373 } 374 375 initializeFromState(); 376 } 377 378 @Override 379 public boolean canApplyTheme() { 380 return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null; 381 } 382 383 @Override 384 public void setHotspot(float x, float y) { 385 if (mHotspot == null) { 386 mHotspot = new Ripple(this, mHotspotBounds, x, y); 387 388 if (mActive) { 389 activateHotspot(); 390 } 391 } else { 392 mHotspot.move(x, y); 393 } 394 } 395 396 /** 397 * Creates an active hotspot at the specified location. 398 */ 399 private void activateHotspot() { 400 if (mAnimatingRipplesCount >= MAX_RIPPLES) { 401 // This should never happen unless the user is tapping like a maniac 402 // or there is a bug that's preventing ripples from being removed. 403 Log.d(LOG_TAG, "Max ripple count exceeded", new RuntimeException()); 404 return; 405 } 406 407 if (mHotspot == null) { 408 final float x = mHotspotBounds.exactCenterX(); 409 final float y = mHotspotBounds.exactCenterY(); 410 mHotspot = new Ripple(this, mHotspotBounds, x, y); 411 } 412 413 final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 414 mHotspot.setup(mState.mMaxRadius, color, mDensity); 415 mHotspot.enter(); 416 417 if (mAnimatingRipples == null) { 418 mAnimatingRipples = new Ripple[MAX_RIPPLES]; 419 } 420 mAnimatingRipples[mAnimatingRipplesCount++] = mHotspot; 421 } 422 423 private void removeHotspot() { 424 if (mHotspot != null) { 425 mHotspot.exit(); 426 mHotspot = null; 427 } 428 } 429 430 private void clearHotspots() { 431 if (mHotspot != null) { 432 mHotspot.cancel(); 433 mHotspot = null; 434 } 435 436 final int count = mAnimatingRipplesCount; 437 final Ripple[] ripples = mAnimatingRipples; 438 for (int i = 0; i < count; i++) { 439 // Calling cancel may remove the ripple from the animating ripple 440 // array, so cache the reference before nulling it out. 441 final Ripple ripple = ripples[i]; 442 ripples[i] = null; 443 ripple.cancel(); 444 } 445 446 mAnimatingRipplesCount = 0; 447 invalidateSelf(); 448 } 449 450 @Override 451 public void setHotspotBounds(int left, int top, int right, int bottom) { 452 mOverrideBounds = true; 453 mHotspotBounds.set(left, top, right, bottom); 454 455 onHotspotBoundsChanged(); 456 } 457 458 /** 459 * Notifies all the animating ripples that the hotspot bounds have changed. 460 */ 461 private void onHotspotBoundsChanged() { 462 final int count = mAnimatingRipplesCount; 463 final Ripple[] ripples = mAnimatingRipples; 464 for (int i = 0; i < count; i++) { 465 ripples[i].onHotspotBoundsChanged(); 466 } 467 } 468 469 @Override 470 public void draw(Canvas canvas) { 471 final boolean isProjected = isProjected(); 472 final boolean hasMask = mMask != null; 473 final boolean drawNonMaskContent = mLayerState.mNum > (hasMask ? 1 : 0); 474 final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE; 475 final Rect bounds = isProjected ? getDirtyBounds() : getBounds(); 476 477 // If we have content, draw it into a layer first. 478 final int contentLayer = drawNonMaskContent ? 479 drawContentLayer(canvas, bounds, SRC_OVER) : -1; 480 481 // Next, try to draw the ripples (into a layer if necessary). If we need 482 // to mask against the underlying content, set the xfermode to SRC_ATOP. 483 final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP; 484 final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode); 485 486 // If we have ripples and a non-opaque mask, draw the masking layer. 487 if (rippleLayer >= 0 && drawMask) { 488 drawMaskingLayer(canvas, bounds, DST_IN); 489 } 490 491 // Composite the layers if needed. 492 if (contentLayer >= 0) { 493 canvas.restoreToCount(contentLayer); 494 } else if (rippleLayer >= 0) { 495 canvas.restoreToCount(rippleLayer); 496 } 497 } 498 499 /** 500 * Removes a ripple from the animating ripple list. 501 * 502 * @param ripple the ripple to remove 503 */ 504 void removeRipple(Ripple ripple) { 505 // Ripple ripple ripple ripple. Ripple ripple. 506 final Ripple[] ripples = mAnimatingRipples; 507 final int count = mAnimatingRipplesCount; 508 final int index = getRippleIndex(ripple); 509 if (index >= 0) { 510 for (int i = index + 1; i < count; i++) { 511 ripples[i - 1] = ripples[i]; 512 } 513 ripples[count - 1] = null; 514 mAnimatingRipplesCount--; 515 invalidateSelf(); 516 } 517 } 518 519 private int getRippleIndex(Ripple ripple) { 520 final Ripple[] ripples = mAnimatingRipples; 521 final int count = mAnimatingRipplesCount; 522 for (int i = 0; i < count; i++) { 523 if (ripples[i] == ripple) { 524 return i; 525 } 526 } 527 return -1; 528 } 529 530 private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 531 final ChildDrawable[] array = mLayerState.mChildren; 532 final int count = mLayerState.mNum; 533 534 // We don't need a layer if we don't expect to draw any ripples, we have 535 // an explicit mask, or if the non-mask content is all opaque. 536 boolean needsLayer = false; 537 if (mAnimatingRipplesCount > 0 && mMask == null) { 538 for (int i = 0; i < count; i++) { 539 if (array[i].mId != R.id.mask 540 && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) { 541 needsLayer = true; 542 break; 543 } 544 } 545 } 546 547 final Paint maskingPaint = getMaskingPaint(mode); 548 final int restoreToCount = needsLayer ? canvas.saveLayer(bounds.left, bounds.top, 549 bounds.right, bounds.bottom, maskingPaint) : -1; 550 551 // Draw everything except the mask. 552 for (int i = 0; i < count; i++) { 553 if (array[i].mId != R.id.mask) { 554 array[i].mDrawable.draw(canvas); 555 } 556 } 557 558 return restoreToCount; 559 } 560 561 private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 562 final int count = mAnimatingRipplesCount; 563 if (count == 0) { 564 return -1; 565 } 566 567 // Separate the ripple color and alpha channel. The alpha will be 568 // applied when we merge the ripples down to the canvas. 569 final int rippleARGB; 570 if (mState.mColor != null) { 571 rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 572 } else { 573 rippleARGB = Color.TRANSPARENT; 574 } 575 576 if (mRipplePaint == null) { 577 mRipplePaint = new Paint(); 578 mRipplePaint.setAntiAlias(true); 579 } 580 581 final int rippleAlpha = Color.alpha(rippleARGB); 582 final Paint ripplePaint = mRipplePaint; 583 ripplePaint.setColor(rippleARGB); 584 ripplePaint.setAlpha(0xFF); 585 586 boolean drewRipples = false; 587 int restoreToCount = -1; 588 int restoreTranslate = -1; 589 590 // Draw ripples and update the animating ripples array. 591 final Ripple[] ripples = mAnimatingRipples; 592 for (int i = 0; i < count; i++) { 593 final Ripple ripple = ripples[i]; 594 595 // If we're masking the ripple layer, make sure we have a layer 596 // first. This will merge SRC_OVER (directly) onto the canvas. 597 if (restoreToCount < 0) { 598 final Paint maskingPaint = getMaskingPaint(mode); 599 maskingPaint.setAlpha(rippleAlpha); 600 restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 601 bounds.right, bounds.bottom, maskingPaint); 602 603 restoreTranslate = canvas.save(); 604 // Translate the canvas to the current hotspot bounds. 605 canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY()); 606 } 607 608 drewRipples |= ripple.draw(canvas, ripplePaint); 609 } 610 611 // Always restore the translation. 612 if (restoreTranslate >= 0) { 613 canvas.restoreToCount(restoreTranslate); 614 } 615 616 // If we created a layer with no content, merge it immediately. 617 if (restoreToCount >= 0 && !drewRipples) { 618 canvas.restoreToCount(restoreToCount); 619 restoreToCount = -1; 620 } 621 622 return restoreToCount; 623 } 624 625 private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 626 final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 627 bounds.right, bounds.bottom, getMaskingPaint(mode)); 628 629 // Ensure that DST_IN blends using the entire layer. 630 canvas.drawColor(Color.TRANSPARENT); 631 632 mMask.draw(canvas); 633 634 return restoreToCount; 635 } 636 637 private Paint getMaskingPaint(PorterDuffXfermode xfermode) { 638 if (mMaskingPaint == null) { 639 mMaskingPaint = new Paint(); 640 } 641 mMaskingPaint.setXfermode(xfermode); 642 mMaskingPaint.setAlpha(0xFF); 643 return mMaskingPaint; 644 } 645 646 @Override 647 public Rect getDirtyBounds() { 648 if (isProjected()) { 649 final Rect drawingBounds = mDrawingBounds; 650 final Rect dirtyBounds = mDirtyBounds; 651 dirtyBounds.set(drawingBounds); 652 drawingBounds.setEmpty(); 653 654 final int cX = (int) mHotspotBounds.exactCenterX(); 655 final int cY = (int) mHotspotBounds.exactCenterY(); 656 final Rect rippleBounds = mTempRect; 657 final Ripple[] activeRipples = mAnimatingRipples; 658 final int N = mAnimatingRipplesCount; 659 for (int i = 0; i < N; i++) { 660 activeRipples[i].getBounds(rippleBounds); 661 rippleBounds.offset(cX, cY); 662 drawingBounds.union(rippleBounds); 663 } 664 665 dirtyBounds.union(drawingBounds); 666 dirtyBounds.union(super.getDirtyBounds()); 667 return dirtyBounds; 668 } else { 669 return getBounds(); 670 } 671 } 672 673 @Override 674 public ConstantState getConstantState() { 675 return mState; 676 } 677 678 static class RippleState extends LayerState { 679 int[] mTouchThemeAttrs; 680 ColorStateList mColor = null; 681 int mMaxRadius = RADIUS_AUTO; 682 683 public RippleState(RippleState orig, RippleDrawable owner, Resources res) { 684 super(orig, owner, res); 685 686 if (orig != null) { 687 mTouchThemeAttrs = orig.mTouchThemeAttrs; 688 mColor = orig.mColor; 689 mMaxRadius = orig.mMaxRadius; 690 } 691 } 692 693 @Override 694 public boolean canApplyTheme() { 695 return mTouchThemeAttrs != null || super.canApplyTheme(); 696 } 697 698 @Override 699 public Drawable newDrawable() { 700 return new RippleDrawable(this, null, null); 701 } 702 703 @Override 704 public Drawable newDrawable(Resources res) { 705 return new RippleDrawable(this, res, null); 706 } 707 708 @Override 709 public Drawable newDrawable(Resources res, Theme theme) { 710 return new RippleDrawable(this, res, theme); 711 } 712 } 713 714 /** 715 * Sets the maximum ripple radius in pixels. The default value of 716 * {@link #RADIUS_AUTO} defines the radius as the distance from the center 717 * of the drawable bounds (or hotspot bounds, if specified) to a corner. 718 * 719 * @param maxRadius the maximum ripple radius in pixels or 720 * {@link #RADIUS_AUTO} to automatically determine the maximum 721 * radius based on the bounds 722 * @see #getMaxRadius() 723 * @see #setHotspotBounds(int, int, int, int) 724 * @hide 725 */ 726 public void setMaxRadius(int maxRadius) { 727 if (maxRadius != RADIUS_AUTO && maxRadius < 0) { 728 throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0"); 729 } 730 731 mState.mMaxRadius = maxRadius; 732 } 733 734 /** 735 * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if 736 * the radius is determined automatically 737 * @see #setMaxRadius(int) 738 * @hide 739 */ 740 public int getMaxRadius() { 741 return mState.mMaxRadius; 742 } 743 744 private RippleDrawable(RippleState state, Resources res, Theme theme) { 745 boolean needsTheme = false; 746 747 final RippleState ns; 748 if (theme != null && state != null && state.canApplyTheme()) { 749 ns = new RippleState(state, this, res); 750 needsTheme = true; 751 } else if (state == null) { 752 ns = new RippleState(null, this, res); 753 } else { 754 // We always need a new state since child drawables contain local 755 // state but live within the parent's constant state. 756 // TODO: Move child drawables into local state. 757 ns = new RippleState(state, this, res); 758 } 759 760 if (res != null) { 761 mDensity = res.getDisplayMetrics().density; 762 } 763 764 mState = ns; 765 mLayerState = ns; 766 767 if (ns.mNum > 0) { 768 ensurePadding(); 769 } 770 771 if (needsTheme) { 772 applyTheme(theme); 773 } 774 775 initializeFromState(); 776 } 777 778 private void initializeFromState() { 779 // Initialize from constant state. 780 mMask = findDrawableByLayerId(R.id.mask); 781 } 782} 783