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