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