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