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