RippleDrawable.java revision a7b64e8eefec1a200701443622debf1032291bdd
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; 36 37import com.android.internal.R; 38 39import org.xmlpull.v1.XmlPullParser; 40import org.xmlpull.v1.XmlPullParserException; 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 * Whether the next draw MUST draw something to canvas. Used to work around 161 * a bug in hardware invalidation following a render thread-accelerated 162 * animation. 163 */ 164 private boolean mNeedsDraw; 165 166 /** 167 * Constructor used for drawable inflation. 168 */ 169 RippleDrawable() { 170 this(new RippleState(null, null, null), null, null); 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(mask, null, android.R.id.mask, 0, 0, 0, 0); 195 } 196 197 setColor(color); 198 ensurePadding(); 199 initializeFromState(); 200 } 201 202 @Override 203 public void jumpToCurrentState() { 204 super.jumpToCurrentState(); 205 206 boolean needsDraw = false; 207 208 if (mRipple != null) { 209 needsDraw |= mRipple.isHardwareAnimating(); 210 mRipple.jump(); 211 } 212 213 if (mBackground != null) { 214 needsDraw |= mBackground.isHardwareAnimating(); 215 mBackground.jump(); 216 } 217 218 needsDraw |= cancelExitingRipples(); 219 220 mNeedsDraw = needsDraw; 221 invalidateSelf(); 222 } 223 224 private boolean cancelExitingRipples() { 225 boolean needsDraw = false; 226 227 final int count = mExitingRipplesCount; 228 final Ripple[] ripples = mExitingRipples; 229 for (int i = 0; i < count; i++) { 230 needsDraw |= ripples[i].isHardwareAnimating(); 231 ripples[i].cancel(); 232 } 233 234 if (ripples != null) { 235 Arrays.fill(ripples, 0, count, null); 236 } 237 mExitingRipplesCount = 0; 238 239 return needsDraw; 240 } 241 242 @Override 243 public void setAlpha(int alpha) { 244 super.setAlpha(alpha); 245 246 // TODO: Should we support this? 247 } 248 249 @Override 250 public void setColorFilter(ColorFilter cf) { 251 super.setColorFilter(cf); 252 253 // TODO: Should we support this? 254 } 255 256 @Override 257 public int getOpacity() { 258 // Worst-case scenario. 259 return PixelFormat.TRANSLUCENT; 260 } 261 262 @Override 263 protected boolean onStateChange(int[] stateSet) { 264 final boolean changed = super.onStateChange(stateSet); 265 266 boolean enabled = false; 267 boolean pressed = false; 268 boolean focused = false; 269 270 for (int state : stateSet) { 271 if (state == R.attr.state_enabled) { 272 enabled = true; 273 } 274 if (state == R.attr.state_focused) { 275 focused = true; 276 } 277 if (state == R.attr.state_pressed) { 278 pressed = true; 279 } 280 } 281 282 setRippleActive(enabled && pressed); 283 setBackgroundActive(focused || (enabled && pressed), focused); 284 285 return changed; 286 } 287 288 private void setRippleActive(boolean active) { 289 if (mRippleActive != active) { 290 mRippleActive = active; 291 if (active) { 292 tryRippleEnter(); 293 } else { 294 tryRippleExit(); 295 } 296 } 297 } 298 299 private void setBackgroundActive(boolean active, boolean focused) { 300 if (mBackgroundActive != active) { 301 mBackgroundActive = active; 302 if (active) { 303 tryBackgroundEnter(focused); 304 } else { 305 tryBackgroundExit(); 306 } 307 } 308 } 309 310 @Override 311 protected void onBoundsChange(Rect bounds) { 312 super.onBoundsChange(bounds); 313 314 if (!mOverrideBounds) { 315 mHotspotBounds.set(bounds); 316 onHotspotBoundsChanged(); 317 } 318 319 invalidateSelf(); 320 } 321 322 @Override 323 public boolean setVisible(boolean visible, boolean restart) { 324 final boolean changed = super.setVisible(visible, restart); 325 326 if (!visible) { 327 clearHotspots(); 328 } else if (changed) { 329 // If we just became visible, ensure the background and ripple 330 // visibilities are consistent with their internal states. 331 if (mRippleActive) { 332 tryRippleEnter(); 333 } 334 335 if (mBackgroundActive) { 336 tryBackgroundEnter(false); 337 } 338 339 // Skip animations, just show the correct final states. 340 jumpToCurrentState(); 341 } 342 343 return changed; 344 } 345 346 /** 347 * @hide 348 */ 349 @Override 350 public boolean isProjected() { 351 // Always project ripples. We'll handle bounding in draw(). 352 return true; 353 } 354 355 @Override 356 public boolean isStateful() { 357 return true; 358 } 359 360 public void setColor(ColorStateList color) { 361 mState.mColor = color; 362 invalidateSelf(); 363 } 364 365 @Override 366 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 367 throws XmlPullParserException, IOException { 368 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); 369 updateStateFromTypedArray(a); 370 a.recycle(); 371 372 // Force padding default to STACK before inflating. 373 setPaddingMode(PADDING_MODE_STACK); 374 375 super.inflate(r, parser, attrs, theme); 376 377 setTargetDensity(r.getDisplayMetrics()); 378 initializeFromState(); 379 } 380 381 @Override 382 public boolean setDrawableByLayerId(int id, Drawable drawable) { 383 if (super.setDrawableByLayerId(id, drawable)) { 384 if (id == R.id.mask) { 385 mMask = drawable; 386 } 387 388 return true; 389 } 390 391 return false; 392 } 393 394 /** 395 * Specifies how layer padding should affect the bounds of subsequent 396 * layers. The default and recommended value for RippleDrawable is 397 * {@link #PADDING_MODE_STACK}. 398 * 399 * @param mode padding mode, one of: 400 * <ul> 401 * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the 402 * padding of the previous layer 403 * <li>{@link #PADDING_MODE_STACK} to stack each layer directly 404 * atop the previous layer 405 * </ul> 406 * @see #getPaddingMode() 407 */ 408 @Override 409 public void setPaddingMode(int mode) { 410 super.setPaddingMode(mode); 411 } 412 413 /** 414 * Initializes the constant state from the values in the typed array. 415 */ 416 private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { 417 final RippleState state = mState; 418 419 // Account for any configuration changes. 420 state.mChangingConfigurations |= a.getChangingConfigurations(); 421 422 // Extract the theme attributes, if any. 423 state.mTouchThemeAttrs = a.extractThemeAttrs(); 424 425 final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color); 426 if (color != null) { 427 mState.mColor = color; 428 } 429 430 verifyRequiredAttributes(a); 431 } 432 433 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 434 if (mState.mColor == null && (mState.mTouchThemeAttrs == null 435 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) { 436 throw new XmlPullParserException(a.getPositionDescription() + 437 ": <ripple> requires a valid color attribute"); 438 } 439 } 440 441 /** 442 * Set the density at which this drawable will be rendered. 443 * 444 * @param metrics The display metrics for this drawable. 445 */ 446 private void setTargetDensity(DisplayMetrics metrics) { 447 if (mDensity != metrics.density) { 448 mDensity = metrics.density; 449 invalidateSelf(); 450 } 451 } 452 453 @Override 454 public void applyTheme(Theme t) { 455 super.applyTheme(t); 456 457 final RippleState state = mState; 458 if (state == null || state.mTouchThemeAttrs == null) { 459 return; 460 } 461 462 final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, 463 R.styleable.RippleDrawable); 464 try { 465 updateStateFromTypedArray(a); 466 } catch (XmlPullParserException e) { 467 throw new RuntimeException(e); 468 } finally { 469 a.recycle(); 470 } 471 472 initializeFromState(); 473 } 474 475 @Override 476 public boolean canApplyTheme() { 477 return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null; 478 } 479 480 @Override 481 public void setHotspot(float x, float y) { 482 if (mRipple == null || mBackground == null) { 483 mPendingX = x; 484 mPendingY = y; 485 mHasPending = true; 486 } 487 488 if (mRipple != null) { 489 mRipple.move(x, y); 490 } 491 } 492 493 /** 494 * Creates an active hotspot at the specified location. 495 */ 496 private void tryBackgroundEnter(boolean focused) { 497 if (mBackground == null) { 498 mBackground = new RippleBackground(this, mHotspotBounds); 499 } 500 501 final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 502 mBackground.setup(mState.mMaxRadius, color, mDensity); 503 mBackground.enter(focused); 504 } 505 506 private void tryBackgroundExit() { 507 if (mBackground != null) { 508 // Don't null out the background, we need it to draw! 509 mBackground.exit(); 510 } 511 } 512 513 /** 514 * Attempts to start an enter animation for the active hotspot. Fails if 515 * there are too many animating ripples. 516 */ 517 private void tryRippleEnter() { 518 if (mExitingRipplesCount >= MAX_RIPPLES) { 519 // This should never happen unless the user is tapping like a maniac 520 // or there is a bug that's preventing ripples from being removed. 521 return; 522 } 523 524 if (mRipple == null) { 525 final float x; 526 final float y; 527 if (mHasPending) { 528 mHasPending = false; 529 x = mPendingX; 530 y = mPendingY; 531 } else { 532 x = mHotspotBounds.exactCenterX(); 533 y = mHotspotBounds.exactCenterY(); 534 } 535 mRipple = new Ripple(this, mHotspotBounds, x, y); 536 } 537 538 final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 539 mRipple.setup(mState.mMaxRadius, color, mDensity); 540 mRipple.enter(); 541 } 542 543 /** 544 * Attempts to start an exit animation for the active hotspot. Fails if 545 * there is no active hotspot. 546 */ 547 private void tryRippleExit() { 548 if (mRipple != null) { 549 if (mExitingRipples == null) { 550 mExitingRipples = new Ripple[MAX_RIPPLES]; 551 } 552 mExitingRipples[mExitingRipplesCount++] = mRipple; 553 mRipple.exit(); 554 mRipple = null; 555 } 556 } 557 558 /** 559 * Cancels and removes the active ripple, all exiting ripples, and the 560 * background. Nothing will be drawn after this method is called. 561 */ 562 private void clearHotspots() { 563 boolean needsDraw = false; 564 565 if (mRipple != null) { 566 needsDraw |= mRipple.isHardwareAnimating(); 567 mRipple.cancel(); 568 mRipple = null; 569 } 570 571 if (mBackground != null) { 572 needsDraw |= mBackground.isHardwareAnimating(); 573 mBackground.cancel(); 574 mBackground = null; 575 } 576 577 needsDraw |= cancelExitingRipples(); 578 579 mNeedsDraw = needsDraw; 580 invalidateSelf(); 581 } 582 583 @Override 584 public void setHotspotBounds(int left, int top, int right, int bottom) { 585 mOverrideBounds = true; 586 mHotspotBounds.set(left, top, right, bottom); 587 588 onHotspotBoundsChanged(); 589 } 590 591 /** @hide */ 592 @Override 593 public void getHotspotBounds(Rect outRect) { 594 outRect.set(mHotspotBounds); 595 } 596 597 /** 598 * Notifies all the animating ripples that the hotspot bounds have changed. 599 */ 600 private void onHotspotBoundsChanged() { 601 final int count = mExitingRipplesCount; 602 final Ripple[] ripples = mExitingRipples; 603 for (int i = 0; i < count; i++) { 604 ripples[i].onHotspotBoundsChanged(); 605 } 606 607 if (mRipple != null) { 608 mRipple.onHotspotBoundsChanged(); 609 } 610 611 if (mBackground != null) { 612 mBackground.onHotspotBoundsChanged(); 613 } 614 } 615 616 /** 617 * Populates <code>outline</code> with the first available layer outline, 618 * excluding the mask layer. 619 * 620 * @param outline Outline in which to place the first available layer outline 621 */ 622 @Override 623 public void getOutline(@NonNull Outline outline) { 624 final LayerState state = mLayerState; 625 final ChildDrawable[] children = state.mChildren; 626 final int N = state.mNum; 627 for (int i = 0; i < N; i++) { 628 if (children[i].mId != R.id.mask) { 629 children[i].mDrawable.getOutline(outline); 630 if (!outline.isEmpty()) return; 631 } 632 } 633 } 634 635 @Override 636 public void draw(@NonNull Canvas canvas) { 637 final boolean hasMask = mMask != null; 638 final boolean drawNonMaskContent = mLayerState.mNum > (hasMask ? 1 : 0); 639 final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE; 640 final Rect bounds = getDirtyBounds(); 641 final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 642 canvas.clipRect(bounds); 643 644 // If we have content, draw it into a layer first. 645 final int contentLayer; 646 if (drawNonMaskContent) { 647 contentLayer = drawContentLayer(canvas, bounds, SRC_OVER); 648 } else { 649 contentLayer = -1; 650 } 651 652 // Next, try to draw the ripples (into a layer if necessary). If we need 653 // to mask against the underlying content, set the xfermode to SRC_ATOP. 654 final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP; 655 656 // If we have a background and a non-opaque mask, draw the masking layer. 657 final int backgroundLayer = drawBackgroundLayer(canvas, bounds, xfermode, drawMask); 658 if (backgroundLayer >= 0) { 659 if (drawMask) { 660 drawMaskingLayer(canvas, bounds, DST_IN); 661 } 662 canvas.restoreToCount(backgroundLayer); 663 } 664 665 // If we have ripples and a non-opaque mask, draw the masking layer. 666 final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode); 667 if (rippleLayer >= 0) { 668 if (drawMask) { 669 drawMaskingLayer(canvas, bounds, DST_IN); 670 } 671 canvas.restoreToCount(rippleLayer); 672 } 673 674 // If we failed to draw anything and we just canceled animations, at 675 // least draw a color so that hardware invalidation works correctly. 676 if (contentLayer < 0 && backgroundLayer < 0 && rippleLayer < 0 && mNeedsDraw) { 677 canvas.drawColor(Color.TRANSPARENT); 678 679 // Request another draw so we can avoid adding a transparent layer 680 // during the next display list refresh. 681 invalidateSelf(); 682 } 683 mNeedsDraw = false; 684 685 canvas.restoreToCount(saveCount); 686 } 687 688 /** 689 * Removes a ripple from the exiting ripple list. 690 * 691 * @param ripple the ripple to remove 692 */ 693 void removeRipple(Ripple ripple) { 694 // Ripple ripple ripple ripple. Ripple ripple. 695 final Ripple[] ripples = mExitingRipples; 696 final int count = mExitingRipplesCount; 697 final int index = getRippleIndex(ripple); 698 if (index >= 0) { 699 System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1)); 700 ripples[count - 1] = null; 701 mExitingRipplesCount--; 702 703 invalidateSelf(); 704 } 705 } 706 707 private int getRippleIndex(Ripple ripple) { 708 final Ripple[] ripples = mExitingRipples; 709 final int count = mExitingRipplesCount; 710 for (int i = 0; i < count; i++) { 711 if (ripples[i] == ripple) { 712 return i; 713 } 714 } 715 return -1; 716 } 717 718 private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 719 final ChildDrawable[] array = mLayerState.mChildren; 720 final int count = mLayerState.mNum; 721 722 // We don't need a layer if we don't expect to draw any ripples or 723 // a background, we have an explicit mask, or if the non-mask content 724 // is all opaque. 725 boolean needsLayer = false; 726 if ((mExitingRipplesCount > 0 || (mBackground != null && mBackground.shouldDraw())) 727 && mMask == null) { 728 for (int i = 0; i < count; i++) { 729 if (array[i].mId != R.id.mask 730 && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) { 731 needsLayer = true; 732 break; 733 } 734 } 735 } 736 737 final Paint maskingPaint = getMaskingPaint(mode); 738 final int restoreToCount = needsLayer ? canvas.saveLayer(bounds.left, bounds.top, 739 bounds.right, bounds.bottom, maskingPaint) : -1; 740 741 // Draw everything except the mask. 742 for (int i = 0; i < count; i++) { 743 if (array[i].mId != R.id.mask) { 744 array[i].mDrawable.draw(canvas); 745 } 746 } 747 748 return restoreToCount; 749 } 750 751 private int drawBackgroundLayer( 752 Canvas canvas, Rect bounds, PorterDuffXfermode mode, boolean drawMask) { 753 int saveCount = -1; 754 755 if (mBackground != null && mBackground.shouldDraw()) { 756 // TODO: We can avoid saveLayer here if we push the xfermode into 757 // the background's render thread animator at exit() time. 758 if (drawMask || mode != SRC_OVER) { 759 saveCount = canvas.saveLayer(bounds.left, bounds.top, bounds.right, 760 bounds.bottom, getMaskingPaint(mode)); 761 } 762 763 final float x = mHotspotBounds.exactCenterX(); 764 final float y = mHotspotBounds.exactCenterY(); 765 canvas.translate(x, y); 766 mBackground.draw(canvas, getRipplePaint()); 767 canvas.translate(-x, -y); 768 } 769 770 return saveCount; 771 } 772 773 private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 774 boolean drewRipples = false; 775 int restoreToCount = -1; 776 int restoreTranslate = -1; 777 778 // Draw ripples and update the animating ripples array. 779 final int count = mExitingRipplesCount; 780 final Ripple[] ripples = mExitingRipples; 781 for (int i = 0; i <= count; i++) { 782 final Ripple ripple; 783 if (i < count) { 784 ripple = ripples[i]; 785 } else if (mRipple != null) { 786 ripple = mRipple; 787 } else { 788 continue; 789 } 790 791 // If we're masking the ripple layer, make sure we have a layer 792 // first. This will merge SRC_OVER (directly) onto the canvas. 793 if (restoreToCount < 0) { 794 final Paint maskingPaint = getMaskingPaint(mode); 795 final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 796 final int alpha = Color.alpha(color); 797 maskingPaint.setAlpha(alpha / 2); 798 799 // TODO: We can avoid saveLayer here if we're only drawing one 800 // ripple and we don't have content or a translucent mask. 801 restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 802 bounds.right, bounds.bottom, maskingPaint); 803 804 // Translate the canvas to the current hotspot bounds. 805 restoreTranslate = canvas.save(); 806 canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY()); 807 } 808 809 drewRipples |= ripple.draw(canvas, getRipplePaint()); 810 } 811 812 // Always restore the translation. 813 if (restoreTranslate >= 0) { 814 canvas.restoreToCount(restoreTranslate); 815 } 816 817 // If we created a layer with no content, merge it immediately. 818 if (restoreToCount >= 0 && !drewRipples) { 819 canvas.restoreToCount(restoreToCount); 820 restoreToCount = -1; 821 } 822 823 return restoreToCount; 824 } 825 826 private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 827 final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 828 bounds.right, bounds.bottom, getMaskingPaint(mode)); 829 830 // Ensure that DST_IN blends using the entire layer. 831 canvas.drawColor(Color.TRANSPARENT); 832 833 mMask.draw(canvas); 834 835 return restoreToCount; 836 } 837 838 private Paint getRipplePaint() { 839 if (mRipplePaint == null) { 840 mRipplePaint = new Paint(); 841 mRipplePaint.setAntiAlias(true); 842 } 843 return mRipplePaint; 844 } 845 846 private Paint getMaskingPaint(PorterDuffXfermode xfermode) { 847 if (mMaskingPaint == null) { 848 mMaskingPaint = new Paint(); 849 } 850 mMaskingPaint.setXfermode(xfermode); 851 mMaskingPaint.setAlpha(0xFF); 852 return mMaskingPaint; 853 } 854 855 @Override 856 public Rect getDirtyBounds() { 857 if (getNumberOfLayers() == 0) { 858 final Rect drawingBounds = mDrawingBounds; 859 final Rect dirtyBounds = mDirtyBounds; 860 dirtyBounds.set(drawingBounds); 861 drawingBounds.setEmpty(); 862 863 final int cX = (int) mHotspotBounds.exactCenterX(); 864 final int cY = (int) mHotspotBounds.exactCenterY(); 865 final Rect rippleBounds = mTempRect; 866 867 final Ripple[] activeRipples = mExitingRipples; 868 final int N = mExitingRipplesCount; 869 for (int i = 0; i < N; i++) { 870 activeRipples[i].getBounds(rippleBounds); 871 rippleBounds.offset(cX, cY); 872 drawingBounds.union(rippleBounds); 873 } 874 875 final RippleBackground background = mBackground; 876 if (background != null) { 877 background.getBounds(rippleBounds); 878 rippleBounds.offset(cX, cY); 879 drawingBounds.union(rippleBounds); 880 } 881 882 dirtyBounds.union(drawingBounds); 883 dirtyBounds.union(super.getDirtyBounds()); 884 return dirtyBounds; 885 } else { 886 return getBounds(); 887 } 888 } 889 890 @Override 891 public ConstantState getConstantState() { 892 return mState; 893 } 894 895 @Override 896 public Drawable mutate() { 897 super.mutate(); 898 899 // LayerDrawable creates a new state using createConstantState, so 900 // this should always be a safe cast. 901 mState = (RippleState) mLayerState; 902 return this; 903 } 904 905 @Override 906 RippleState createConstantState(LayerState state, Resources res) { 907 return new RippleState(state, this, res); 908 } 909 910 static class RippleState extends LayerState { 911 int[] mTouchThemeAttrs; 912 ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); 913 int mMaxRadius = RADIUS_AUTO; 914 915 public RippleState(LayerState orig, RippleDrawable owner, Resources res) { 916 super(orig, owner, res); 917 918 if (orig != null && orig instanceof RippleState) { 919 final RippleState origs = (RippleState) orig; 920 mTouchThemeAttrs = origs.mTouchThemeAttrs; 921 mColor = origs.mColor; 922 mMaxRadius = origs.mMaxRadius; 923 } 924 } 925 926 @Override 927 public boolean canApplyTheme() { 928 return mTouchThemeAttrs != null || super.canApplyTheme(); 929 } 930 931 @Override 932 public Drawable newDrawable() { 933 return new RippleDrawable(this, null, null); 934 } 935 936 @Override 937 public Drawable newDrawable(Resources res) { 938 return new RippleDrawable(this, res, null); 939 } 940 941 @Override 942 public Drawable newDrawable(Resources res, Theme theme) { 943 return new RippleDrawable(this, res, theme); 944 } 945 } 946 947 /** 948 * Sets the maximum ripple radius in pixels. The default value of 949 * {@link #RADIUS_AUTO} defines the radius as the distance from the center 950 * of the drawable bounds (or hotspot bounds, if specified) to a corner. 951 * 952 * @param maxRadius the maximum ripple radius in pixels or 953 * {@link #RADIUS_AUTO} to automatically determine the maximum 954 * radius based on the bounds 955 * @see #getMaxRadius() 956 * @see #setHotspotBounds(int, int, int, int) 957 * @hide 958 */ 959 public void setMaxRadius(int maxRadius) { 960 if (maxRadius != RADIUS_AUTO && maxRadius < 0) { 961 throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0"); 962 } 963 964 mState.mMaxRadius = maxRadius; 965 } 966 967 /** 968 * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if 969 * the radius is determined automatically 970 * @see #setMaxRadius(int) 971 * @hide 972 */ 973 public int getMaxRadius() { 974 return mState.mMaxRadius; 975 } 976 977 private RippleDrawable(RippleState state, Resources res, Theme theme) { 978 boolean needsTheme = false; 979 980 final RippleState ns; 981 if (theme != null && state != null && state.canApplyTheme()) { 982 ns = new RippleState(state, this, res); 983 needsTheme = true; 984 } else if (state == null) { 985 ns = new RippleState(null, this, res); 986 } else { 987 // We always need a new state since child drawables contain local 988 // state but live within the parent's constant state. 989 // TODO: Move child drawables into local state. 990 ns = new RippleState(state, this, res); 991 } 992 993 if (res != null) { 994 mDensity = res.getDisplayMetrics().density; 995 } 996 997 mState = ns; 998 mLayerState = ns; 999 1000 if (ns.mNum > 0) { 1001 ensurePadding(); 1002 } 1003 1004 if (needsTheme) { 1005 applyTheme(theme); 1006 } 1007 1008 initializeFromState(); 1009 } 1010 1011 private void initializeFromState() { 1012 // Initialize from constant state. 1013 mMask = findDrawableByLayerId(R.id.mask); 1014 } 1015} 1016