RippleDrawable.java revision 1b6e856e6f9dab4464e3c556b2f68527439fc329
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 masking layer, e.g. the layer with id R.id.mask. */ 99 private Drawable mMask; 100 101 /** The current hotspot. May be actively animating or pending entry. */ 102 private Ripple mHotspot; 103 104 /** 105 * Lazily-created array of actively animating ripples. Inactive ripples are 106 * pruned during draw(). The locations of these will not change. 107 */ 108 private Ripple[] mAnimatingRipples; 109 private int mAnimatingRipplesCount = 0; 110 111 /** Paint used to control appearance of ripples. */ 112 private Paint mRipplePaint; 113 114 /** Paint used to control reveal layer masking. */ 115 private Paint mMaskingPaint; 116 117 /** Target density of the display into which ripples are drawn. */ 118 private float mDensity = 1.0f; 119 120 /** Whether bounds are being overridden. */ 121 private boolean mOverrideBounds; 122 123 /** Whether the hotspot is currently active (e.g. focused or pressed). */ 124 private boolean mActive; 125 126 RippleDrawable() { 127 this(null, null); 128 } 129 130 /** 131 * Creates a new ripple drawable with the specified content and mask 132 * drawables. 133 * 134 * @param content The content drawable, may be {@code null} 135 * @param mask The mask drawable, may be {@code null} 136 */ 137 public RippleDrawable(Drawable content, Drawable mask) { 138 this(new RippleState(null, null, null), null, null); 139 140 if (content != null) { 141 addLayer(content, null, 0, 0, 0, 0, 0); 142 } 143 144 if (mask != null) { 145 addLayer(content, null, android.R.id.mask, 0, 0, 0, 0); 146 } 147 148 ensurePadding(); 149 } 150 151 @Override 152 public void setAlpha(int alpha) { 153 super.setAlpha(alpha); 154 155 // TODO: Should we support this? 156 } 157 158 @Override 159 public void setColorFilter(ColorFilter cf) { 160 super.setColorFilter(cf); 161 162 // TODO: Should we support this? 163 } 164 165 @Override 166 public int getOpacity() { 167 // Worst-case scenario. 168 return PixelFormat.TRANSLUCENT; 169 } 170 171 @Override 172 protected boolean onStateChange(int[] stateSet) { 173 super.onStateChange(stateSet); 174 175 // TODO: This would make more sense in a StateListDrawable. 176 boolean active = false; 177 boolean enabled = false; 178 final int N = stateSet.length; 179 for (int i = 0; i < N; i++) { 180 if (stateSet[i] == R.attr.state_enabled) { 181 enabled = true; 182 } 183 if (stateSet[i] == R.attr.state_focused 184 || stateSet[i] == R.attr.state_pressed) { 185 active = true; 186 } 187 } 188 setActive(active && enabled); 189 190 // Update the paint color. Only applicable when animated in software. 191 if (mRipplePaint != null && mState.mTint != null) { 192 final ColorStateList stateList = mState.mTint; 193 final int newColor = stateList.getColorForState(stateSet, 0); 194 final int oldColor = mRipplePaint.getColor(); 195 if (oldColor != newColor) { 196 mRipplePaint.setColor(newColor); 197 invalidateSelf(); 198 return true; 199 } 200 } 201 202 return false; 203 } 204 205 private void setActive(boolean active) { 206 if (mActive != active) { 207 mActive = active; 208 209 if (active) { 210 activateHotspot(); 211 } else { 212 removeHotspot(); 213 } 214 } 215 } 216 217 @Override 218 protected void onBoundsChange(Rect bounds) { 219 super.onBoundsChange(bounds); 220 221 if (!mOverrideBounds) { 222 mHotspotBounds.set(bounds); 223 onHotspotBoundsChanged(); 224 } 225 226 invalidateSelf(); 227 } 228 229 @Override 230 public boolean setVisible(boolean visible, boolean restart) { 231 if (!visible) { 232 clearHotspots(); 233 } 234 235 return super.setVisible(visible, restart); 236 } 237 238 /** 239 * @hide 240 */ 241 @Override 242 public boolean isProjected() { 243 return getNumberOfLayers() == 0; 244 } 245 246 @Override 247 public boolean isStateful() { 248 return true; 249 } 250 251 @Override 252 public void setTint(ColorStateList tint, Mode tintMode) { 253 mState.mTint = tint; 254 mState.setTintMode(tintMode); 255 invalidateSelf(); 256 } 257 258 @Override 259 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 260 throws XmlPullParserException, IOException { 261 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); 262 updateStateFromTypedArray(a); 263 a.recycle(); 264 265 // Force padding default to STACK before inflating. 266 setPaddingMode(PADDING_MODE_STACK); 267 268 super.inflate(r, parser, attrs, theme); 269 270 setTargetDensity(r.getDisplayMetrics()); 271 initializeFromState(); 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 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 initializeFromState(); 366 } 367 368 @Override 369 public boolean canApplyTheme() { 370 return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null; 371 } 372 373 @Override 374 public void setHotspot(float x, float y) { 375 if (mState.mPinned && !circleContains(mHotspotBounds, x, y)) { 376 x = mHotspotBounds.exactCenterX(); 377 y = mHotspotBounds.exactCenterY(); 378 } 379 380 if (mHotspot == null) { 381 mHotspot = new Ripple(this, mHotspotBounds, x, y); 382 383 if (mActive) { 384 activateHotspot(); 385 } 386 } else { 387 mHotspot.move(x, y); 388 } 389 } 390 391 private boolean circleContains(Rect bounds, float x, float y) { 392 final float pX = bounds.exactCenterX() - x; 393 final float pY = bounds.exactCenterY() - y; 394 final double pointRadius = Math.sqrt(pX * pX + pY * pY); 395 396 final float bX = bounds.width() / 2.0f; 397 final float bY = bounds.height() / 2.0f; 398 final double boundsRadius = Math.sqrt(bX * bX + bY * bY); 399 400 return pointRadius < boundsRadius; 401 } 402 403 /** 404 * Creates an active hotspot at the specified location. 405 */ 406 private void activateHotspot() { 407 if (mAnimatingRipplesCount >= MAX_RIPPLES) { 408 // This should never happen unless the user is tapping like a maniac 409 // or there is a bug that's preventing ripples from being removed. 410 Log.d(LOG_TAG, "Max ripple count exceeded", new RuntimeException()); 411 return; 412 } 413 414 if (mHotspot == null) { 415 final float x = mHotspotBounds.exactCenterX(); 416 final float y = mHotspotBounds.exactCenterY(); 417 mHotspot = new Ripple(this, mHotspotBounds, x, y); 418 } 419 420 final int color = mState.mTint.getColorForState(getState(), Color.TRANSPARENT); 421 mHotspot.setup(mState.mMaxRadius, color, mDensity); 422 mHotspot.enter(); 423 424 if (mAnimatingRipples == null) { 425 mAnimatingRipples = new Ripple[MAX_RIPPLES]; 426 } 427 mAnimatingRipples[mAnimatingRipplesCount++] = mHotspot; 428 } 429 430 private void removeHotspot() { 431 if (mHotspot != null) { 432 mHotspot.exit(); 433 mHotspot = null; 434 } 435 } 436 437 private void clearHotspots() { 438 if (mHotspot != null) { 439 mHotspot.cancel(); 440 mHotspot = null; 441 } 442 443 final int count = mAnimatingRipplesCount; 444 final Ripple[] ripples = mAnimatingRipples; 445 for (int i = 0; i < count; i++) { 446 // Calling cancel may remove the ripple from the animating ripple 447 // array, so cache the reference before nulling it out. 448 final Ripple ripple = ripples[i]; 449 ripples[i] = null; 450 ripple.cancel(); 451 } 452 453 mAnimatingRipplesCount = 0; 454 invalidateSelf(); 455 } 456 457 @Override 458 public void setHotspotBounds(int left, int top, int right, int bottom) { 459 mOverrideBounds = true; 460 mHotspotBounds.set(left, top, right, bottom); 461 462 onHotspotBoundsChanged(); 463 } 464 465 /** 466 * Notifies all the animating ripples that the hotspot bounds have changed. 467 */ 468 private void onHotspotBoundsChanged() { 469 final int count = mAnimatingRipplesCount; 470 final Ripple[] ripples = mAnimatingRipples; 471 for (int i = 0; i < count; i++) { 472 ripples[i].onHotspotBoundsChanged(); 473 } 474 } 475 476 @Override 477 public void draw(Canvas canvas) { 478 final boolean isProjected = isProjected(); 479 final boolean hasMask = mMask != null; 480 final boolean drawNonMaskContent = mLayerState.mNum > (hasMask ? 1 : 0); 481 final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE; 482 final Rect bounds = isProjected ? getDirtyBounds() : getBounds(); 483 484 // If we have content, draw it into a layer first. 485 final int contentLayer = drawNonMaskContent ? 486 drawContentLayer(canvas, bounds, SRC_OVER) : -1; 487 488 // Next, try to draw the ripples (into a layer if necessary). 489 final int rippleLayer = drawRippleLayer(canvas, bounds, mState.mTintXfermode); 490 491 // If we have ripples and a non-opaque mask, draw the masking layer. 492 if (rippleLayer >= 0 && drawMask) { 493 drawMaskingLayer(canvas, bounds, DST_IN); 494 } 495 496 // Composite the layers if needed. 497 if (contentLayer >= 0) { 498 canvas.restoreToCount(contentLayer); 499 } else if (rippleLayer >= 0) { 500 canvas.restoreToCount(rippleLayer); 501 } 502 } 503 504 /** 505 * Removes a ripple from the animating ripple list. 506 * 507 * @param ripple the ripple to remove 508 */ 509 void removeRipple(Ripple ripple) { 510 // Ripple ripple ripple ripple. Ripple ripple. 511 final Ripple[] ripples = mAnimatingRipples; 512 final int count = mAnimatingRipplesCount; 513 final int index = getRippleIndex(ripple); 514 if (index >= 0) { 515 for (int i = index + 1; i < count; i++) { 516 ripples[i - 1] = ripples[i]; 517 } 518 ripples[count - 1] = null; 519 mAnimatingRipplesCount--; 520 invalidateSelf(); 521 } 522 } 523 524 private int getRippleIndex(Ripple ripple) { 525 final Ripple[] ripples = mAnimatingRipples; 526 final int count = mAnimatingRipplesCount; 527 for (int i = 0; i < count; i++) { 528 if (ripples[i] == ripple) { 529 return i; 530 } 531 } 532 return -1; 533 } 534 535 private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 536 // TODO: We don't need a layer if all the content is opaque. 537 final Paint maskingPaint = getMaskingPaint(mode); 538 final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 539 bounds.right, bounds.bottom, maskingPaint); 540 541 // Draw everything except the mask. 542 final ChildDrawable[] array = mLayerState.mChildren; 543 final int count = mLayerState.mNum; 544 for (int i = 0; i < count; i++) { 545 if (array[i].mId != R.id.mask) { 546 array[i].mDrawable.draw(canvas); 547 } 548 } 549 550 return restoreToCount; 551 } 552 553 private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 554 final int count = mAnimatingRipplesCount; 555 if (count == 0) { 556 return -1; 557 } 558 559 // Separate the ripple color and alpha channel. The alpha will be 560 // applied when we merge the ripples down to the canvas. 561 final int rippleARGB; 562 if (mState.mTint != null) { 563 rippleARGB = mState.mTint.getColorForState(getState(), Color.TRANSPARENT); 564 } else { 565 rippleARGB = Color.TRANSPARENT; 566 } 567 568 if (mRipplePaint == null) { 569 mRipplePaint = new Paint(); 570 mRipplePaint.setAntiAlias(true); 571 } 572 573 final int rippleAlpha = Color.alpha(rippleARGB); 574 final Paint ripplePaint = mRipplePaint; 575 ripplePaint.setColor(rippleARGB); 576 ripplePaint.setAlpha(0xFF); 577 578 boolean drewRipples = false; 579 int restoreToCount = -1; 580 int restoreTranslate = -1; 581 582 // Draw ripples and update the animating ripples array. 583 final Ripple[] ripples = mAnimatingRipples; 584 for (int i = 0; i < count; i++) { 585 final Ripple ripple = ripples[i]; 586 587 // If we're masking the ripple layer, make sure we have a layer 588 // first. This will merge SRC_OVER (directly) onto the canvas. 589 if (restoreToCount < 0) { 590 final Paint maskingPaint = getMaskingPaint(mode); 591 maskingPaint.setAlpha(rippleAlpha); 592 restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 593 bounds.right, bounds.bottom, maskingPaint); 594 595 restoreTranslate = canvas.save(); 596 // Translate the canvas to the current hotspot bounds. 597 canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY()); 598 } 599 600 drewRipples |= ripple.draw(canvas, ripplePaint); 601 } 602 603 // Always restore the translation. 604 if (restoreTranslate >= 0) { 605 canvas.restoreToCount(restoreTranslate); 606 } 607 608 // If we created a layer with no content, merge it immediately. 609 if (restoreToCount >= 0 && !drewRipples) { 610 canvas.restoreToCount(restoreToCount); 611 restoreToCount = -1; 612 } 613 614 return restoreToCount; 615 } 616 617 private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 618 final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 619 bounds.right, bounds.bottom, getMaskingPaint(mode)); 620 621 // Ensure that DST_IN blends using the entire layer. 622 canvas.drawColor(Color.TRANSPARENT); 623 624 mMask.draw(canvas); 625 626 return restoreToCount; 627 } 628 629 private Paint getMaskingPaint(PorterDuffXfermode xfermode) { 630 if (mMaskingPaint == null) { 631 mMaskingPaint = new Paint(); 632 } 633 mMaskingPaint.setXfermode(xfermode); 634 mMaskingPaint.setAlpha(0xFF); 635 return mMaskingPaint; 636 } 637 638 @Override 639 public Rect getDirtyBounds() { 640 final Rect drawingBounds = mDrawingBounds; 641 final Rect dirtyBounds = mDirtyBounds; 642 dirtyBounds.set(drawingBounds); 643 drawingBounds.setEmpty(); 644 645 final int cX = (int) mHotspotBounds.exactCenterX(); 646 final int cY = (int) mHotspotBounds.exactCenterY(); 647 final Rect rippleBounds = mTempRect; 648 final Ripple[] activeRipples = mAnimatingRipples; 649 final int N = mAnimatingRipplesCount; 650 for (int i = 0; i < N; i++) { 651 activeRipples[i].getBounds(rippleBounds); 652 rippleBounds.offset(cX, cY); 653 drawingBounds.union(rippleBounds); 654 } 655 656 dirtyBounds.union(drawingBounds); 657 dirtyBounds.union(super.getDirtyBounds()); 658 return dirtyBounds; 659 } 660 661 @Override 662 public ConstantState getConstantState() { 663 return mState; 664 } 665 666 static class RippleState extends LayerState { 667 int[] mTouchThemeAttrs; 668 ColorStateList mTint = null; 669 PorterDuffXfermode mTintXfermode = SRC_ATOP; 670 int mMaxRadius = RADIUS_AUTO; 671 boolean mPinned = false; 672 673 public RippleState(RippleState orig, RippleDrawable owner, Resources res) { 674 super(orig, owner, res); 675 676 if (orig != null) { 677 mTouchThemeAttrs = orig.mTouchThemeAttrs; 678 mTint = orig.mTint; 679 mTintXfermode = orig.mTintXfermode; 680 mMaxRadius = orig.mMaxRadius; 681 mPinned = orig.mPinned; 682 } 683 } 684 685 public void setTintMode(Mode mode) { 686 mTintXfermode = new PorterDuffXfermode(mode); 687 } 688 689 public PorterDuffXfermode getTintXfermode() { 690 return mTintXfermode; 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