RippleDrawable.java revision cc3c573334a9cd2124a8a0ccf2f37884e36f83fa
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 com.android.internal.R; 20 21import org.xmlpull.v1.XmlPullParser; 22import org.xmlpull.v1.XmlPullParserException; 23 24import android.annotation.NonNull; 25import android.annotation.Nullable; 26import android.content.res.ColorStateList; 27import android.content.res.Resources; 28import android.content.res.Resources.Theme; 29import android.content.res.TypedArray; 30import android.graphics.Canvas; 31import android.graphics.Color; 32import android.graphics.ColorFilter; 33import android.graphics.Outline; 34import android.graphics.Paint; 35import android.graphics.PixelFormat; 36import android.graphics.PorterDuff.Mode; 37import android.graphics.PorterDuffXfermode; 38import android.graphics.Rect; 39import android.util.AttributeSet; 40import android.util.DisplayMetrics; 41 42import java.io.IOException; 43import java.util.Arrays; 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 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 /** Mirrors mLayerState with some extra information. */ 118 private RippleState mState; 119 120 /** The masking layer, e.g. the layer with id R.id.mask. */ 121 private Drawable mMask; 122 123 /** The current background. May be actively animating or pending entry. */ 124 private RippleBackground mBackground; 125 126 /** Whether we expect to draw a background when visible. */ 127 private boolean mBackgroundActive; 128 129 /** The current ripple. May be actively animating or pending entry. */ 130 private Ripple mRipple; 131 132 /** Whether we expect to draw a ripple when visible. */ 133 private boolean mRippleActive; 134 135 // Hotspot coordinates that are awaiting activation. 136 private float mPendingX; 137 private float mPendingY; 138 private boolean mHasPending; 139 140 /** 141 * Lazily-created array of actively animating ripples. Inactive ripples are 142 * pruned during draw(). The locations of these will not change. 143 */ 144 private Ripple[] mExitingRipples; 145 private int mExitingRipplesCount = 0; 146 147 /** Paint used to control appearance of ripples. */ 148 private Paint mRipplePaint; 149 150 /** Paint used to control reveal layer masking. */ 151 private Paint mMaskingPaint; 152 153 /** Target density of the display into which ripples are drawn. */ 154 private float mDensity = 1.0f; 155 156 /** Whether bounds are being overridden. */ 157 private boolean mOverrideBounds; 158 159 /** 160 * Constructor used for drawable inflation. 161 */ 162 RippleDrawable() { 163 this(new RippleState(null, null, null), null); 164 } 165 166 /** 167 * Creates a new ripple drawable with the specified ripple color and 168 * optional content and mask drawables. 169 * 170 * @param color The ripple color 171 * @param content The content drawable, may be {@code null} 172 * @param mask The mask drawable, may be {@code null} 173 */ 174 public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content, 175 @Nullable Drawable mask) { 176 this(new RippleState(null, null, null), null); 177 178 if (color == null) { 179 throw new IllegalArgumentException("RippleDrawable requires a non-null color"); 180 } 181 182 if (content != null) { 183 addLayer(content, null, 0, 0, 0, 0, 0); 184 } 185 186 if (mask != null) { 187 addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0); 188 } 189 190 setColor(color); 191 ensurePadding(); 192 initializeFromState(); 193 } 194 195 @Override 196 public void jumpToCurrentState() { 197 super.jumpToCurrentState(); 198 199 if (mRipple != null) { 200 mRipple.jump(); 201 } 202 203 if (mBackground != null) { 204 mBackground.jump(); 205 } 206 207 cancelExitingRipples(); 208 invalidateSelf(); 209 } 210 211 private boolean cancelExitingRipples() { 212 boolean needsDraw = false; 213 214 final int count = mExitingRipplesCount; 215 final Ripple[] ripples = mExitingRipples; 216 for (int i = 0; i < count; i++) { 217 needsDraw |= ripples[i].isHardwareAnimating(); 218 ripples[i].cancel(); 219 } 220 221 if (ripples != null) { 222 Arrays.fill(ripples, 0, count, null); 223 } 224 mExitingRipplesCount = 0; 225 226 return needsDraw; 227 } 228 229 @Override 230 public void setAlpha(int alpha) { 231 super.setAlpha(alpha); 232 233 // TODO: Should we support this? 234 } 235 236 @Override 237 public void setColorFilter(ColorFilter cf) { 238 super.setColorFilter(cf); 239 240 // TODO: Should we support this? 241 } 242 243 @Override 244 public int getOpacity() { 245 // Worst-case scenario. 246 return PixelFormat.TRANSLUCENT; 247 } 248 249 @Override 250 protected boolean onStateChange(int[] stateSet) { 251 final boolean changed = super.onStateChange(stateSet); 252 253 boolean enabled = false; 254 boolean pressed = false; 255 boolean focused = false; 256 257 for (int state : stateSet) { 258 if (state == R.attr.state_enabled) { 259 enabled = true; 260 } 261 if (state == R.attr.state_focused) { 262 focused = true; 263 } 264 if (state == R.attr.state_pressed) { 265 pressed = true; 266 } 267 } 268 269 setRippleActive(enabled && pressed); 270 setBackgroundActive(focused || (enabled && pressed), focused); 271 272 return changed; 273 } 274 275 private void setRippleActive(boolean active) { 276 if (mRippleActive != active) { 277 mRippleActive = active; 278 if (active) { 279 tryRippleEnter(); 280 } else { 281 tryRippleExit(); 282 } 283 } 284 } 285 286 private void setBackgroundActive(boolean active, boolean focused) { 287 if (mBackgroundActive != active) { 288 mBackgroundActive = active; 289 if (active) { 290 tryBackgroundEnter(focused); 291 } else { 292 tryBackgroundExit(); 293 } 294 } 295 } 296 297 @Override 298 protected void onBoundsChange(Rect bounds) { 299 super.onBoundsChange(bounds); 300 301 if (!mOverrideBounds) { 302 mHotspotBounds.set(bounds); 303 onHotspotBoundsChanged(); 304 } 305 306 invalidateSelf(); 307 } 308 309 @Override 310 public boolean setVisible(boolean visible, boolean restart) { 311 final boolean changed = super.setVisible(visible, restart); 312 313 if (!visible) { 314 clearHotspots(); 315 } else if (changed) { 316 // If we just became visible, ensure the background and ripple 317 // visibilities are consistent with their internal states. 318 if (mRippleActive) { 319 tryRippleEnter(); 320 } 321 322 if (mBackgroundActive) { 323 tryBackgroundEnter(false); 324 } 325 326 // Skip animations, just show the correct final states. 327 jumpToCurrentState(); 328 } 329 330 return changed; 331 } 332 333 /** 334 * @hide 335 */ 336 @Override 337 public boolean isProjected() { 338 return getNumberOfLayers() == 0; 339 } 340 341 @Override 342 public boolean isStateful() { 343 return true; 344 } 345 346 public void setColor(ColorStateList color) { 347 mState.mColor = color; 348 invalidateSelf(); 349 } 350 351 @Override 352 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 353 throws XmlPullParserException, IOException { 354 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); 355 updateStateFromTypedArray(a); 356 a.recycle(); 357 358 // Force padding default to STACK before inflating. 359 setPaddingMode(PADDING_MODE_STACK); 360 361 super.inflate(r, parser, attrs, theme); 362 363 setTargetDensity(r.getDisplayMetrics()); 364 initializeFromState(); 365 } 366 367 @Override 368 public boolean setDrawableByLayerId(int id, Drawable drawable) { 369 if (super.setDrawableByLayerId(id, drawable)) { 370 if (id == R.id.mask) { 371 mMask = drawable; 372 } 373 374 return true; 375 } 376 377 return false; 378 } 379 380 /** 381 * Specifies how layer padding should affect the bounds of subsequent 382 * layers. The default and recommended value for RippleDrawable is 383 * {@link #PADDING_MODE_STACK}. 384 * 385 * @param mode padding mode, one of: 386 * <ul> 387 * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the 388 * padding of the previous layer 389 * <li>{@link #PADDING_MODE_STACK} to stack each layer directly 390 * atop the previous layer 391 * </ul> 392 * @see #getPaddingMode() 393 */ 394 @Override 395 public void setPaddingMode(int mode) { 396 super.setPaddingMode(mode); 397 } 398 399 /** 400 * Initializes the constant state from the values in the typed array. 401 */ 402 private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { 403 final RippleState state = mState; 404 405 // Account for any configuration changes. 406 state.mChangingConfigurations |= a.getChangingConfigurations(); 407 408 // Extract the theme attributes, if any. 409 state.mTouchThemeAttrs = a.extractThemeAttrs(); 410 411 final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color); 412 if (color != null) { 413 mState.mColor = color; 414 } 415 416 verifyRequiredAttributes(a); 417 } 418 419 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 420 if (mState.mColor == null && (mState.mTouchThemeAttrs == null 421 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) { 422 throw new XmlPullParserException(a.getPositionDescription() + 423 ": <ripple> requires a valid color attribute"); 424 } 425 } 426 427 /** 428 * Set the density at which this drawable will be rendered. 429 * 430 * @param metrics The display metrics for this drawable. 431 */ 432 private void setTargetDensity(DisplayMetrics metrics) { 433 if (mDensity != metrics.density) { 434 mDensity = metrics.density; 435 invalidateSelf(); 436 } 437 } 438 439 @Override 440 public void applyTheme(Theme t) { 441 super.applyTheme(t); 442 443 final RippleState state = mState; 444 if (state == null || state.mTouchThemeAttrs == null) { 445 return; 446 } 447 448 final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, 449 R.styleable.RippleDrawable); 450 try { 451 updateStateFromTypedArray(a); 452 } catch (XmlPullParserException e) { 453 throw new RuntimeException(e); 454 } finally { 455 a.recycle(); 456 } 457 458 initializeFromState(); 459 } 460 461 @Override 462 public boolean canApplyTheme() { 463 return (mState != null && mState.canApplyTheme()) || super.canApplyTheme(); 464 } 465 466 @Override 467 public void setHotspot(float x, float y) { 468 if (mRipple == null || mBackground == null) { 469 mPendingX = x; 470 mPendingY = y; 471 mHasPending = true; 472 } 473 474 if (mRipple != null) { 475 mRipple.move(x, y); 476 } 477 } 478 479 /** 480 * Creates an active hotspot at the specified location. 481 */ 482 private void tryBackgroundEnter(boolean focused) { 483 if (mBackground == null) { 484 mBackground = new RippleBackground(this, mHotspotBounds); 485 } 486 487 mBackground.setup(mState.mMaxRadius, mDensity); 488 mBackground.enter(focused); 489 } 490 491 private void tryBackgroundExit() { 492 if (mBackground != null) { 493 // Don't null out the background, we need it to draw! 494 mBackground.exit(); 495 } 496 } 497 498 /** 499 * Attempts to start an enter animation for the active hotspot. Fails if 500 * there are too many animating ripples. 501 */ 502 private void tryRippleEnter() { 503 if (mExitingRipplesCount >= MAX_RIPPLES) { 504 // This should never happen unless the user is tapping like a maniac 505 // or there is a bug that's preventing ripples from being removed. 506 return; 507 } 508 509 if (mRipple == null) { 510 final float x; 511 final float y; 512 if (mHasPending) { 513 mHasPending = false; 514 x = mPendingX; 515 y = mPendingY; 516 } else { 517 x = mHotspotBounds.exactCenterX(); 518 y = mHotspotBounds.exactCenterY(); 519 } 520 mRipple = new Ripple(this, mHotspotBounds, x, y); 521 } 522 523 mRipple.setup(mState.mMaxRadius, mDensity); 524 mRipple.enter(); 525 } 526 527 /** 528 * Attempts to start an exit animation for the active hotspot. Fails if 529 * there is no active hotspot. 530 */ 531 private void tryRippleExit() { 532 if (mRipple != null) { 533 if (mExitingRipples == null) { 534 mExitingRipples = new Ripple[MAX_RIPPLES]; 535 } 536 mExitingRipples[mExitingRipplesCount++] = mRipple; 537 mRipple.exit(); 538 mRipple = null; 539 } 540 } 541 542 /** 543 * Cancels and removes the active ripple, all exiting ripples, and the 544 * background. Nothing will be drawn after this method is called. 545 */ 546 private void clearHotspots() { 547 if (mRipple != null) { 548 mRipple.cancel(); 549 mRipple = null; 550 } 551 552 if (mBackground != null) { 553 mBackground.cancel(); 554 mBackground = null; 555 } 556 557 cancelExitingRipples(); 558 invalidateSelf(); 559 } 560 561 @Override 562 public void setHotspotBounds(int left, int top, int right, int bottom) { 563 mOverrideBounds = true; 564 mHotspotBounds.set(left, top, right, bottom); 565 566 onHotspotBoundsChanged(); 567 } 568 569 /** @hide */ 570 @Override 571 public void getHotspotBounds(Rect outRect) { 572 outRect.set(mHotspotBounds); 573 } 574 575 /** 576 * Notifies all the animating ripples that the hotspot bounds have changed. 577 */ 578 private void onHotspotBoundsChanged() { 579 final int count = mExitingRipplesCount; 580 final Ripple[] ripples = mExitingRipples; 581 for (int i = 0; i < count; i++) { 582 ripples[i].onHotspotBoundsChanged(); 583 } 584 585 if (mRipple != null) { 586 mRipple.onHotspotBoundsChanged(); 587 } 588 589 if (mBackground != null) { 590 mBackground.onHotspotBoundsChanged(); 591 } 592 } 593 594 /** 595 * Populates <code>outline</code> with the first available layer outline, 596 * excluding the mask layer. 597 * 598 * @param outline Outline in which to place the first available layer outline 599 */ 600 @Override 601 public void getOutline(@NonNull Outline outline) { 602 final LayerState state = mLayerState; 603 final ChildDrawable[] children = state.mChildren; 604 final int N = state.mNum; 605 for (int i = 0; i < N; i++) { 606 if (children[i].mId != R.id.mask) { 607 children[i].mDrawable.getOutline(outline); 608 if (!outline.isEmpty()) return; 609 } 610 } 611 } 612 613 /** 614 * Optimized for drawing ripples with a mask layer and optional content. 615 */ 616 @Override 617 public void draw(@NonNull Canvas canvas) { 618 final boolean hasMask = mMask != null; 619 final boolean hasRipples = mRipple != null || mExitingRipplesCount > 0 620 || (mBackground != null && mBackground.shouldDraw()); 621 622 // Clip to the dirty bounds, which will be the drawable bounds if we 623 // have a mask or content and the ripple bounds if we're projecting. 624 final Rect bounds = getDirtyBounds(); 625 final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 626 canvas.clipRect(bounds); 627 628 // If we have content, draw it first. If we have ripples and no mask, 629 // we'll draw it into a SRC_OVER layer so that we can mask ripples 630 // against it using SRC_IN. 631 final boolean hasContentLayer = drawContent(canvas, bounds, hasRipples, hasMask); 632 633 // Next, try to draw the ripples. If we have a non-opaque mask, we'll 634 // draw the ripples into a SRC_OVER layer, draw the mask into a DST_IN 635 // layer, and blend. 636 if (hasRipples) { 637 final boolean hasNonOpaqueMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE; 638 final boolean hasRippleLayer = drawBackgroundAndRipples(canvas, bounds, 639 hasNonOpaqueMask, hasContentLayer); 640 641 // If drawing ripples created a layer, we have a non-opaque mask 642 // that needs to be blended on top of the ripples with DST_IN. 643 if (hasRippleLayer) { 644 drawMaskingLayer(canvas, bounds, DST_IN); 645 } 646 } 647 648 canvas.restoreToCount(saveCount); 649 } 650 651 /** 652 * Removes a ripple from the exiting ripple list. 653 * 654 * @param ripple the ripple to remove 655 */ 656 void removeRipple(Ripple ripple) { 657 // Ripple ripple ripple ripple. Ripple ripple. 658 final Ripple[] ripples = mExitingRipples; 659 final int count = mExitingRipplesCount; 660 final int index = getRippleIndex(ripple); 661 if (index >= 0) { 662 System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1)); 663 ripples[count - 1] = null; 664 mExitingRipplesCount--; 665 666 invalidateSelf(); 667 } 668 } 669 670 private int getRippleIndex(Ripple ripple) { 671 final Ripple[] ripples = mExitingRipples; 672 final int count = mExitingRipplesCount; 673 for (int i = 0; i < count; i++) { 674 if (ripples[i] == ripple) { 675 return i; 676 } 677 } 678 return -1; 679 } 680 681 private boolean drawContent(Canvas canvas, Rect bounds, boolean hasRipples, boolean hasMask) { 682 final ChildDrawable[] array = mLayerState.mChildren; 683 final int count = mLayerState.mNum; 684 685 boolean needsLayer = false; 686 687 if (hasRipples && !hasMask) { 688 // If we only have opaque content, we don't really need a layer 689 // because the ripples will be clipped to the drawable bounds. 690 for (int i = 0; i < count; i++) { 691 if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) { 692 needsLayer = true; 693 break; 694 } 695 } 696 } 697 698 if (needsLayer) { 699 canvas.saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, 700 getMaskingPaint(SRC_OVER)); 701 } 702 703 // Draw everything except the mask. 704 for (int i = 0; i < count; i++) { 705 if (array[i].mId != R.id.mask) { 706 array[i].mDrawable.draw(canvas); 707 } 708 } 709 710 return needsLayer; 711 } 712 713 private boolean drawBackgroundAndRipples( 714 Canvas canvas, Rect bounds, boolean hasNonOpaqueMask, boolean hasContentLayer) { 715 if (hasNonOpaqueMask) { 716 final Paint p = getMaskingPaint(SRC_OVER); 717 canvas.saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, p); 718 } 719 720 final PorterDuffXfermode mode = hasContentLayer ? SRC_ATOP : SRC_OVER; 721 final float x = mHotspotBounds.exactCenterX(); 722 final float y = mHotspotBounds.exactCenterY(); 723 canvas.translate(x, y); 724 725 final Paint p = getRipplePaint(); 726 p.setXfermode(mode); 727 728 // Grab the color for the current state and cut the alpha channel in 729 // half so that the ripple and background together yield full alpha. 730 final int color = mState.mColor.getColorForState(getState(), Color.BLACK); 731 final int alpha = (Color.alpha(color) / 2) << 24; 732 p.setColor(color & 0xFFFFFF | alpha); 733 734 final RippleBackground background = mBackground; 735 if (background != null && background.shouldDraw()) { 736 background.draw(canvas, p); 737 } 738 739 final int count = mExitingRipplesCount; 740 if (count > 0) { 741 final Ripple[] ripples = mExitingRipples; 742 for (int i = 0; i < count; i++) { 743 ripples[i].draw(canvas, p); 744 } 745 } 746 747 final Ripple active = mRipple; 748 if (active != null) { 749 active.draw(canvas, p); 750 } 751 752 canvas.translate(-x, -y); 753 754 // Returns true if a layer was created. 755 return hasNonOpaqueMask; 756 } 757 758 private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 759 final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 760 bounds.right, bounds.bottom, getMaskingPaint(mode)); 761 762 // Ensure that DST_IN blends using the entire layer. 763 canvas.drawColor(Color.TRANSPARENT); 764 765 mMask.draw(canvas); 766 767 return restoreToCount; 768 } 769 770 private Paint getRipplePaint() { 771 if (mRipplePaint == null) { 772 mRipplePaint = new Paint(); 773 mRipplePaint.setAntiAlias(true); 774 mRipplePaint.setStyle(Paint.Style.FILL); 775 } 776 return mRipplePaint; 777 } 778 779 private Paint getMaskingPaint(PorterDuffXfermode xfermode) { 780 if (mMaskingPaint == null) { 781 mMaskingPaint = new Paint(); 782 } 783 mMaskingPaint.setXfermode(xfermode); 784 mMaskingPaint.setAlpha(0xFF); 785 return mMaskingPaint; 786 } 787 788 @Override 789 public Rect getDirtyBounds() { 790 if (isProjected()) { 791 final Rect drawingBounds = mDrawingBounds; 792 final Rect dirtyBounds = mDirtyBounds; 793 dirtyBounds.set(drawingBounds); 794 drawingBounds.setEmpty(); 795 796 final int cX = (int) mHotspotBounds.exactCenterX(); 797 final int cY = (int) mHotspotBounds.exactCenterY(); 798 final Rect rippleBounds = mTempRect; 799 800 final Ripple[] activeRipples = mExitingRipples; 801 final int N = mExitingRipplesCount; 802 for (int i = 0; i < N; i++) { 803 activeRipples[i].getBounds(rippleBounds); 804 rippleBounds.offset(cX, cY); 805 drawingBounds.union(rippleBounds); 806 } 807 808 final RippleBackground background = mBackground; 809 if (background != null) { 810 background.getBounds(rippleBounds); 811 rippleBounds.offset(cX, cY); 812 drawingBounds.union(rippleBounds); 813 } 814 815 dirtyBounds.union(drawingBounds); 816 dirtyBounds.union(super.getDirtyBounds()); 817 return dirtyBounds; 818 } else { 819 return getBounds(); 820 } 821 } 822 823 @Override 824 public ConstantState getConstantState() { 825 return mState; 826 } 827 828 @Override 829 public Drawable mutate() { 830 super.mutate(); 831 832 // LayerDrawable creates a new state using createConstantState, so 833 // this should always be a safe cast. 834 mState = (RippleState) mLayerState; 835 return this; 836 } 837 838 @Override 839 RippleState createConstantState(LayerState state, Resources res) { 840 return new RippleState(state, this, res); 841 } 842 843 static class RippleState extends LayerState { 844 int[] mTouchThemeAttrs; 845 ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); 846 int mMaxRadius = RADIUS_AUTO; 847 848 public RippleState(LayerState orig, RippleDrawable owner, Resources res) { 849 super(orig, owner, res); 850 851 if (orig != null && orig instanceof RippleState) { 852 final RippleState origs = (RippleState) orig; 853 mTouchThemeAttrs = origs.mTouchThemeAttrs; 854 mColor = origs.mColor; 855 mMaxRadius = origs.mMaxRadius; 856 } 857 } 858 859 @Override 860 public boolean canApplyTheme() { 861 return mTouchThemeAttrs != null || super.canApplyTheme(); 862 } 863 864 @Override 865 public Drawable newDrawable() { 866 return new RippleDrawable(this, null); 867 } 868 869 @Override 870 public Drawable newDrawable(Resources res) { 871 return new RippleDrawable(this, res); 872 } 873 } 874 875 /** 876 * Sets the maximum ripple radius in pixels. The default value of 877 * {@link #RADIUS_AUTO} defines the radius as the distance from the center 878 * of the drawable bounds (or hotspot bounds, if specified) to a corner. 879 * 880 * @param maxRadius the maximum ripple radius in pixels or 881 * {@link #RADIUS_AUTO} to automatically determine the maximum 882 * radius based on the bounds 883 * @see #getMaxRadius() 884 * @see #setHotspotBounds(int, int, int, int) 885 * @hide 886 */ 887 public void setMaxRadius(int maxRadius) { 888 if (maxRadius != RADIUS_AUTO && maxRadius < 0) { 889 throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0"); 890 } 891 892 mState.mMaxRadius = maxRadius; 893 } 894 895 /** 896 * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if 897 * the radius is determined automatically 898 * @see #setMaxRadius(int) 899 * @hide 900 */ 901 public int getMaxRadius() { 902 return mState.mMaxRadius; 903 } 904 905 private RippleDrawable(RippleState state, Resources res) { 906 mState = new RippleState(state, this, res); 907 mLayerState = mState; 908 909 if (mState.mNum > 0) { 910 ensurePadding(); 911 } 912 913 if (res != null) { 914 mDensity = res.getDisplayMetrics().density; 915 } 916 917 initializeFromState(); 918 } 919 920 private void initializeFromState() { 921 // Initialize from constant state. 922 mMask = findDrawableByLayerId(R.id.mask); 923 } 924} 925