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