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