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