RippleDrawable.java revision 6dfa60f33ca6018959ebff1efde82db7d2aed1e3
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.graphics.drawable; 18 19import com.android.internal.R; 20 21import org.xmlpull.v1.XmlPullParser; 22import org.xmlpull.v1.XmlPullParserException; 23 24import android.annotation.NonNull; 25import android.annotation.Nullable; 26import android.content.res.ColorStateList; 27import android.content.res.Resources; 28import android.content.res.Resources.Theme; 29import android.content.res.TypedArray; 30import android.graphics.Bitmap; 31import android.graphics.BitmapShader; 32import android.graphics.Canvas; 33import android.graphics.Color; 34import android.graphics.ColorFilter; 35import android.graphics.Matrix; 36import android.graphics.Outline; 37import android.graphics.Paint; 38import android.graphics.PixelFormat; 39import android.graphics.PorterDuff; 40import android.graphics.PorterDuffColorFilter; 41import android.graphics.Rect; 42import android.graphics.Shader; 43import android.util.AttributeSet; 44import android.util.DisplayMetrics; 45 46import java.io.IOException; 47import java.util.Arrays; 48 49/** 50 * Drawable that shows a ripple effect in response to state changes. The 51 * anchoring position of the ripple for a given state may be specified by 52 * calling {@link #setHotspot(float, float)} with the corresponding state 53 * attribute identifier. 54 * <p> 55 * A touch feedback drawable may contain multiple child layers, including a 56 * special mask layer that is not drawn to the screen. A single layer may be set 57 * as the mask by specifying its android:id value as {@link android.R.id#mask}. 58 * <pre> 59 * <code><!-- A red ripple masked against an opaque rectangle. --/> 60 * <ripple android:color="#ffff0000"> 61 * <item android:id="@android:id/mask" 62 * android:drawable="@android:color/white" /> 63 * </ripple></code> 64 * </pre> 65 * <p> 66 * If a mask layer is set, the ripple effect will be masked against that layer 67 * before it is drawn over the composite of the remaining child layers. 68 * <p> 69 * If no mask layer is set, the ripple effect is masked against the composite 70 * of the child layers. 71 * <pre> 72 * <code><!-- A green ripple drawn atop a black rectangle. --/> 73 * <ripple android:color="#ff00ff00"> 74 * <item android:drawable="@android:color/black" /> 75 * </ripple> 76 * 77 * <!-- A blue ripple drawn atop a drawable resource. --/> 78 * <ripple android:color="#ff0000ff"> 79 * <item android:drawable="@drawable/my_drawable" /> 80 * </ripple></code> 81 * </pre> 82 * <p> 83 * If no child layers or mask is specified and the ripple is set as a View 84 * background, the ripple will be drawn atop the first available parent 85 * background within the View's hierarchy. In this case, the drawing region 86 * may extend outside of the Drawable bounds. 87 * <pre> 88 * <code><!-- An unbounded red ripple. --/> 89 * <ripple android:color="#ffff0000" /></code> 90 * </pre> 91 * 92 * @attr ref android.R.styleable#RippleDrawable_color 93 */ 94public class RippleDrawable extends LayerDrawable { 95 private static final int MASK_UNKNOWN = -1; 96 private static final int MASK_NONE = 0; 97 private static final int MASK_CONTENT = 1; 98 private static final int MASK_EXPLICIT = 2; 99 100 /** 101 * Constant for automatically determining the maximum ripple radius. 102 * 103 * @see #setMaxRadius(int) 104 * @hide 105 */ 106 public static final int RADIUS_AUTO = -1; 107 108 /** The maximum number of ripples supported. */ 109 private static final int MAX_RIPPLES = 10; 110 111 private final Rect mTempRect = new Rect(); 112 113 /** Current ripple effect bounds, used to constrain ripple effects. */ 114 private final Rect mHotspotBounds = new Rect(); 115 116 /** Current drawing bounds, used to compute dirty region. */ 117 private final Rect mDrawingBounds = new Rect(); 118 119 /** Current dirty bounds, union of current and previous drawing bounds. */ 120 private final Rect mDirtyBounds = new Rect(); 121 122 /** Mirrors mLayerState with some extra information. */ 123 private RippleState mState; 124 125 /** The masking layer, e.g. the layer with id R.id.mask. */ 126 private Drawable mMask; 127 128 /** The current background. May be actively animating or pending entry. */ 129 private RippleBackground mBackground; 130 131 private Bitmap mMaskBuffer; 132 private BitmapShader mMaskShader; 133 private Canvas mMaskCanvas; 134 private Matrix mMaskMatrix; 135 private PorterDuffColorFilter mMaskColorFilter; 136 private boolean mHasValidMask; 137 138 /** Whether we expect to draw a background when visible. */ 139 private boolean mBackgroundActive; 140 141 /** The current ripple. May be actively animating or pending entry. */ 142 private Ripple mRipple; 143 144 /** Whether we expect to draw a ripple when visible. */ 145 private boolean mRippleActive; 146 147 // Hotspot coordinates that are awaiting activation. 148 private float mPendingX; 149 private float mPendingY; 150 private boolean mHasPending; 151 152 /** 153 * Lazily-created array of actively animating ripples. Inactive ripples are 154 * pruned during draw(). The locations of these will not change. 155 */ 156 private Ripple[] mExitingRipples; 157 private int mExitingRipplesCount = 0; 158 159 /** Paint used to control appearance of ripples. */ 160 private Paint mRipplePaint; 161 162 /** Target density of the display into which ripples are drawn. */ 163 private float mDensity = 1.0f; 164 165 /** Whether bounds are being overridden. */ 166 private boolean mOverrideBounds; 167 168 /** 169 * Constructor used for drawable inflation. 170 */ 171 RippleDrawable() { 172 this(new RippleState(null, null, null), null); 173 } 174 175 /** 176 * Creates a new ripple drawable with the specified ripple color and 177 * optional content and mask drawables. 178 * 179 * @param color The ripple color 180 * @param content The content drawable, may be {@code null} 181 * @param mask The mask drawable, may be {@code null} 182 */ 183 public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content, 184 @Nullable Drawable mask) { 185 this(new RippleState(null, null, null), null); 186 187 if (color == null) { 188 throw new IllegalArgumentException("RippleDrawable requires a non-null color"); 189 } 190 191 if (content != null) { 192 addLayer(content, null, 0, 0, 0, 0, 0); 193 } 194 195 if (mask != null) { 196 addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0); 197 } 198 199 setColor(color); 200 ensurePadding(); 201 initializeFromState(); 202 } 203 204 @Override 205 public void jumpToCurrentState() { 206 super.jumpToCurrentState(); 207 208 if (mRipple != null) { 209 mRipple.jump(); 210 } 211 212 if (mBackground != null) { 213 mBackground.jump(); 214 } 215 216 cancelExitingRipples(); 217 invalidateSelf(); 218 } 219 220 private boolean cancelExitingRipples() { 221 boolean needsDraw = false; 222 223 final int count = mExitingRipplesCount; 224 final Ripple[] ripples = mExitingRipples; 225 for (int i = 0; i < count; i++) { 226 needsDraw |= ripples[i].isHardwareAnimating(); 227 ripples[i].cancel(); 228 } 229 230 if (ripples != null) { 231 Arrays.fill(ripples, 0, count, null); 232 } 233 mExitingRipplesCount = 0; 234 235 return needsDraw; 236 } 237 238 @Override 239 public void setAlpha(int alpha) { 240 super.setAlpha(alpha); 241 242 // TODO: Should we support this? 243 } 244 245 @Override 246 public void setColorFilter(ColorFilter cf) { 247 super.setColorFilter(cf); 248 249 // TODO: Should we support this? 250 } 251 252 @Override 253 public int getOpacity() { 254 // Worst-case scenario. 255 return PixelFormat.TRANSLUCENT; 256 } 257 258 @Override 259 protected boolean onStateChange(int[] stateSet) { 260 final boolean changed = super.onStateChange(stateSet); 261 262 boolean enabled = false; 263 boolean pressed = false; 264 boolean focused = false; 265 266 for (int state : stateSet) { 267 if (state == R.attr.state_enabled) { 268 enabled = true; 269 } 270 if (state == R.attr.state_focused) { 271 focused = true; 272 } 273 if (state == R.attr.state_pressed) { 274 pressed = true; 275 } 276 } 277 278 setRippleActive(enabled && pressed); 279 setBackgroundActive(focused || (enabled && pressed), focused); 280 281 return changed; 282 } 283 284 private void setRippleActive(boolean active) { 285 if (mRippleActive != active) { 286 mRippleActive = active; 287 if (active) { 288 tryRippleEnter(); 289 } else { 290 tryRippleExit(); 291 } 292 } 293 } 294 295 private void setBackgroundActive(boolean active, boolean focused) { 296 if (mBackgroundActive != active) { 297 mBackgroundActive = active; 298 if (active) { 299 tryBackgroundEnter(focused); 300 } else { 301 tryBackgroundExit(); 302 } 303 } 304 } 305 306 @Override 307 protected void onBoundsChange(Rect bounds) { 308 super.onBoundsChange(bounds); 309 310 if (!mOverrideBounds) { 311 mHotspotBounds.set(bounds); 312 onHotspotBoundsChanged(); 313 } 314 315 invalidateSelf(); 316 } 317 318 @Override 319 public boolean setVisible(boolean visible, boolean restart) { 320 final boolean changed = super.setVisible(visible, restart); 321 322 if (!visible) { 323 clearHotspots(); 324 } else if (changed) { 325 // If we just became visible, ensure the background and ripple 326 // visibilities are consistent with their internal states. 327 if (mRippleActive) { 328 tryRippleEnter(); 329 } 330 331 if (mBackgroundActive) { 332 tryBackgroundEnter(false); 333 } 334 335 // Skip animations, just show the correct final states. 336 jumpToCurrentState(); 337 } 338 339 return changed; 340 } 341 342 /** 343 * @hide 344 */ 345 @Override 346 public boolean isProjected() { 347 return getNumberOfLayers() == 0; 348 } 349 350 @Override 351 public boolean isStateful() { 352 return true; 353 } 354 355 public void setColor(ColorStateList color) { 356 mState.mColor = color; 357 invalidateSelf(); 358 } 359 360 @Override 361 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 362 throws XmlPullParserException, IOException { 363 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); 364 updateStateFromTypedArray(a); 365 a.recycle(); 366 367 // Force padding default to STACK before inflating. 368 setPaddingMode(PADDING_MODE_STACK); 369 370 super.inflate(r, parser, attrs, theme); 371 372 setTargetDensity(r.getDisplayMetrics()); 373 initializeFromState(); 374 } 375 376 @Override 377 public boolean setDrawableByLayerId(int id, Drawable drawable) { 378 if (super.setDrawableByLayerId(id, drawable)) { 379 if (id == R.id.mask) { 380 mMask = drawable; 381 } 382 383 return true; 384 } 385 386 return false; 387 } 388 389 /** 390 * Specifies how layer padding should affect the bounds of subsequent 391 * layers. The default and recommended value for RippleDrawable is 392 * {@link #PADDING_MODE_STACK}. 393 * 394 * @param mode padding mode, one of: 395 * <ul> 396 * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the 397 * padding of the previous layer 398 * <li>{@link #PADDING_MODE_STACK} to stack each layer directly 399 * atop the previous layer 400 * </ul> 401 * @see #getPaddingMode() 402 */ 403 @Override 404 public void setPaddingMode(int mode) { 405 super.setPaddingMode(mode); 406 } 407 408 /** 409 * Initializes the constant state from the values in the typed array. 410 */ 411 private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { 412 final RippleState state = mState; 413 414 // Account for any configuration changes. 415 state.mChangingConfigurations |= a.getChangingConfigurations(); 416 417 // Extract the theme attributes, if any. 418 state.mTouchThemeAttrs = a.extractThemeAttrs(); 419 420 final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color); 421 if (color != null) { 422 mState.mColor = color; 423 } 424 425 verifyRequiredAttributes(a); 426 } 427 428 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 429 if (mState.mColor == null && (mState.mTouchThemeAttrs == null 430 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) { 431 throw new XmlPullParserException(a.getPositionDescription() + 432 ": <ripple> requires a valid color attribute"); 433 } 434 } 435 436 /** 437 * Set the density at which this drawable will be rendered. 438 * 439 * @param metrics The display metrics for this drawable. 440 */ 441 private void setTargetDensity(DisplayMetrics metrics) { 442 if (mDensity != metrics.density) { 443 mDensity = metrics.density; 444 invalidateSelf(); 445 } 446 } 447 448 @Override 449 public void applyTheme(Theme t) { 450 super.applyTheme(t); 451 452 final RippleState state = mState; 453 if (state == null || state.mTouchThemeAttrs == null) { 454 return; 455 } 456 457 final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, 458 R.styleable.RippleDrawable); 459 try { 460 updateStateFromTypedArray(a); 461 } catch (XmlPullParserException e) { 462 throw new RuntimeException(e); 463 } finally { 464 a.recycle(); 465 } 466 467 initializeFromState(); 468 } 469 470 @Override 471 public boolean canApplyTheme() { 472 return (mState != null && mState.canApplyTheme()) || super.canApplyTheme(); 473 } 474 475 @Override 476 public void setHotspot(float x, float y) { 477 if (mRipple == null || mBackground == null) { 478 mPendingX = x; 479 mPendingY = y; 480 mHasPending = true; 481 } 482 483 if (mRipple != null) { 484 mRipple.move(x, y); 485 } 486 } 487 488 /** 489 * Creates an active hotspot at the specified location. 490 */ 491 private void tryBackgroundEnter(boolean focused) { 492 if (mBackground == null) { 493 mBackground = new RippleBackground(this, mHotspotBounds); 494 } 495 496 mBackground.setup(mState.mMaxRadius, mDensity); 497 mBackground.enter(focused); 498 } 499 500 private void tryBackgroundExit() { 501 if (mBackground != null) { 502 // Don't null out the background, we need it to draw! 503 mBackground.exit(); 504 } 505 } 506 507 /** 508 * Attempts to start an enter animation for the active hotspot. Fails if 509 * there are too many animating ripples. 510 */ 511 private void tryRippleEnter() { 512 if (mExitingRipplesCount >= MAX_RIPPLES) { 513 // This should never happen unless the user is tapping like a maniac 514 // or there is a bug that's preventing ripples from being removed. 515 return; 516 } 517 518 if (mRipple == null) { 519 final float x; 520 final float y; 521 if (mHasPending) { 522 mHasPending = false; 523 x = mPendingX; 524 y = mPendingY; 525 } else { 526 x = mHotspotBounds.exactCenterX(); 527 y = mHotspotBounds.exactCenterY(); 528 } 529 mRipple = new Ripple(this, mHotspotBounds, x, y); 530 } 531 532 mRipple.setup(mState.mMaxRadius, mDensity); 533 mRipple.enter(); 534 } 535 536 /** 537 * Attempts to start an exit animation for the active hotspot. Fails if 538 * there is no active hotspot. 539 */ 540 private void tryRippleExit() { 541 if (mRipple != null) { 542 if (mExitingRipples == null) { 543 mExitingRipples = new Ripple[MAX_RIPPLES]; 544 } 545 mExitingRipples[mExitingRipplesCount++] = mRipple; 546 mRipple.exit(); 547 mRipple = null; 548 } 549 } 550 551 /** 552 * Cancels and removes the active ripple, all exiting ripples, and the 553 * background. Nothing will be drawn after this method is called. 554 */ 555 private void clearHotspots() { 556 if (mRipple != null) { 557 mRipple.cancel(); 558 mRipple = null; 559 } 560 561 if (mBackground != null) { 562 mBackground.cancel(); 563 mBackground = null; 564 } 565 566 cancelExitingRipples(); 567 invalidateSelf(); 568 } 569 570 @Override 571 public void setHotspotBounds(int left, int top, int right, int bottom) { 572 mOverrideBounds = true; 573 mHotspotBounds.set(left, top, right, bottom); 574 575 onHotspotBoundsChanged(); 576 } 577 578 /** @hide */ 579 @Override 580 public void getHotspotBounds(Rect outRect) { 581 outRect.set(mHotspotBounds); 582 } 583 584 /** 585 * Notifies all the animating ripples that the hotspot bounds have changed. 586 */ 587 private void onHotspotBoundsChanged() { 588 final int count = mExitingRipplesCount; 589 final Ripple[] ripples = mExitingRipples; 590 for (int i = 0; i < count; i++) { 591 ripples[i].onHotspotBoundsChanged(); 592 } 593 594 if (mRipple != null) { 595 mRipple.onHotspotBoundsChanged(); 596 } 597 598 if (mBackground != null) { 599 mBackground.onHotspotBoundsChanged(); 600 } 601 } 602 603 /** 604 * Populates <code>outline</code> with the first available layer outline, 605 * excluding the mask layer. 606 * 607 * @param outline Outline in which to place the first available layer outline 608 */ 609 @Override 610 public void getOutline(@NonNull Outline outline) { 611 final LayerState state = mLayerState; 612 final ChildDrawable[] children = state.mChildren; 613 final int N = state.mNum; 614 for (int i = 0; i < N; i++) { 615 if (children[i].mId != R.id.mask) { 616 children[i].mDrawable.getOutline(outline); 617 if (!outline.isEmpty()) return; 618 } 619 } 620 } 621 622 /** 623 * Optimized for drawing ripples with a mask layer and optional content. 624 */ 625 @Override 626 public void draw(@NonNull Canvas canvas) { 627 // Clip to the dirty bounds, which will be the drawable bounds if we 628 // have a mask or content and the ripple bounds if we're projecting. 629 final Rect bounds = getDirtyBounds(); 630 final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 631 canvas.clipRect(bounds); 632 633 drawContent(canvas); 634 drawBackgroundAndRipples(canvas); 635 636 canvas.restoreToCount(saveCount); 637 } 638 639 @Override 640 public void invalidateSelf() { 641 super.invalidateSelf(); 642 643 // Force the mask to update on the next draw(). 644 mHasValidMask = false; 645 } 646 647 /** 648 * @return whether we need to use a mask 649 */ 650 private void updateMaskShaderIfNeeded() { 651 if (mHasValidMask) { 652 return; 653 } 654 655 final int maskType = getMaskType(); 656 if (maskType == MASK_UNKNOWN) { 657 return; 658 } 659 660 mHasValidMask = true; 661 662 if (maskType == MASK_NONE) { 663 if (mMaskBuffer != null) { 664 mMaskBuffer.recycle(); 665 mMaskBuffer = null; 666 mMaskShader = null; 667 mMaskCanvas = null; 668 } 669 mMaskMatrix = null; 670 mMaskColorFilter = null; 671 return; 672 } 673 674 // Ensure we have a correctly-sized buffer. 675 final Rect bounds = getBounds(); 676 if (mMaskBuffer == null 677 || mMaskBuffer.getWidth() != bounds.width() 678 || mMaskBuffer.getHeight() != bounds.height()) { 679 if (mMaskBuffer != null) { 680 mMaskBuffer.recycle(); 681 } 682 683 mMaskBuffer = Bitmap.createBitmap( 684 bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); 685 mMaskShader = new BitmapShader(mMaskBuffer, 686 Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 687 mMaskCanvas = new Canvas(mMaskBuffer); 688 } else { 689 mMaskBuffer.eraseColor(Color.TRANSPARENT); 690 } 691 692 if (mMaskMatrix == null) { 693 mMaskMatrix = new Matrix(); 694 } else { 695 mMaskMatrix.reset(); 696 } 697 698 if (mMaskColorFilter == null) { 699 mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN); 700 } 701 702 // Draw the appropriate mask. 703 if (maskType == MASK_EXPLICIT) { 704 drawMask(mMaskCanvas); 705 } else if (maskType == MASK_CONTENT) { 706 drawContent(mMaskCanvas); 707 } 708 } 709 710 private int getMaskType() { 711 if (mRipple == null && mExitingRipplesCount <= 0 712 && (mBackground == null || !mBackground.shouldDraw())) { 713 // We might need a mask later. 714 return MASK_UNKNOWN; 715 } 716 717 if (mMask != null) { 718 if (mMask.getOpacity() == PixelFormat.OPAQUE) { 719 // Clipping handles opaque explicit masks. 720 return MASK_NONE; 721 } else { 722 return MASK_EXPLICIT; 723 } 724 } 725 726 // Check for non-opaque, non-mask content. 727 final ChildDrawable[] array = mLayerState.mChildren; 728 final int count = mLayerState.mNum; 729 for (int i = 0; i < count; i++) { 730 if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) { 731 return MASK_CONTENT; 732 } 733 } 734 735 // Clipping handles opaque content. 736 return MASK_NONE; 737 } 738 739 /** 740 * Removes a ripple from the exiting ripple list. 741 * 742 * @param ripple the ripple to remove 743 */ 744 void removeRipple(Ripple ripple) { 745 // Ripple ripple ripple ripple. Ripple ripple. 746 final Ripple[] ripples = mExitingRipples; 747 final int count = mExitingRipplesCount; 748 final int index = getRippleIndex(ripple); 749 if (index >= 0) { 750 System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1)); 751 ripples[count - 1] = null; 752 mExitingRipplesCount--; 753 754 invalidateSelf(); 755 } 756 } 757 758 private int getRippleIndex(Ripple ripple) { 759 final Ripple[] ripples = mExitingRipples; 760 final int count = mExitingRipplesCount; 761 for (int i = 0; i < count; i++) { 762 if (ripples[i] == ripple) { 763 return i; 764 } 765 } 766 return -1; 767 } 768 769 private void drawContent(Canvas canvas) { 770 // Draw everything except the mask. 771 final ChildDrawable[] array = mLayerState.mChildren; 772 final int count = mLayerState.mNum; 773 for (int i = 0; i < count; i++) { 774 if (array[i].mId != R.id.mask) { 775 array[i].mDrawable.draw(canvas); 776 } 777 } 778 } 779 780 private void drawBackgroundAndRipples(Canvas canvas) { 781 final Ripple active = mRipple; 782 final RippleBackground background = mBackground; 783 final int count = mExitingRipplesCount; 784 if (active == null && count <= 0 && (background == null || !background.shouldDraw())) { 785 // Move along, nothing to draw here. 786 return; 787 } 788 789 final float x = mHotspotBounds.exactCenterX(); 790 final float y = mHotspotBounds.exactCenterY(); 791 canvas.translate(x, y); 792 793 updateMaskShaderIfNeeded(); 794 795 // Position the shader to account for canvas translation. 796 if (mMaskShader != null) { 797 mMaskMatrix.setTranslate(-x, -y); 798 mMaskShader.setLocalMatrix(mMaskMatrix); 799 } 800 801 // Grab the color for the current state and cut the alpha channel in 802 // half so that the ripple and background together yield full alpha. 803 final int color = mState.mColor.getColorForState(getState(), Color.BLACK); 804 final int halfAlpha = (Color.alpha(color) / 2) << 24; 805 final Paint p = getRipplePaint(); 806 807 if (mMaskColorFilter != null) { 808 // The ripple timing depends on the paint's alpha value, so we need 809 // to push just the alpha channel into the paint and let the filter 810 // handle the full-alpha color. 811 final int fullAlphaColor = color | (0xFF << 24); 812 mMaskColorFilter.setColor(fullAlphaColor); 813 814 p.setColor(halfAlpha); 815 p.setColorFilter(mMaskColorFilter); 816 p.setShader(mMaskShader); 817 } else { 818 final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha; 819 p.setColor(halfAlphaColor); 820 p.setColorFilter(null); 821 p.setShader(null); 822 } 823 824 if (background != null && background.shouldDraw()) { 825 background.draw(canvas, p); 826 } 827 828 if (count > 0) { 829 final Ripple[] ripples = mExitingRipples; 830 for (int i = 0; i < count; i++) { 831 ripples[i].draw(canvas, p); 832 } 833 } 834 835 if (active != null) { 836 active.draw(canvas, p); 837 } 838 839 canvas.translate(-x, -y); 840 } 841 842 private void drawMask(Canvas canvas) { 843 mMask.draw(canvas); 844 } 845 846 private Paint getRipplePaint() { 847 if (mRipplePaint == null) { 848 mRipplePaint = new Paint(); 849 mRipplePaint.setAntiAlias(true); 850 mRipplePaint.setStyle(Paint.Style.FILL); 851 } 852 return mRipplePaint; 853 } 854 855 @Override 856 public Rect getDirtyBounds() { 857 if (isProjected()) { 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 903 // The locally cached drawable may have changed. 904 mMask = findDrawableByLayerId(R.id.mask); 905 906 return this; 907 } 908 909 @Override 910 RippleState createConstantState(LayerState state, Resources res) { 911 return new RippleState(state, this, res); 912 } 913 914 static class RippleState extends LayerState { 915 int[] mTouchThemeAttrs; 916 ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); 917 int mMaxRadius = RADIUS_AUTO; 918 919 public RippleState(LayerState orig, RippleDrawable owner, Resources res) { 920 super(orig, owner, res); 921 922 if (orig != null && orig instanceof RippleState) { 923 final RippleState origs = (RippleState) orig; 924 mTouchThemeAttrs = origs.mTouchThemeAttrs; 925 mColor = origs.mColor; 926 mMaxRadius = origs.mMaxRadius; 927 } 928 } 929 930 @Override 931 public boolean canApplyTheme() { 932 return mTouchThemeAttrs != null || super.canApplyTheme(); 933 } 934 935 @Override 936 public Drawable newDrawable() { 937 return new RippleDrawable(this, null); 938 } 939 940 @Override 941 public Drawable newDrawable(Resources res) { 942 return new RippleDrawable(this, res); 943 } 944 } 945 946 /** 947 * Sets the maximum ripple radius in pixels. The default value of 948 * {@link #RADIUS_AUTO} defines the radius as the distance from the center 949 * of the drawable bounds (or hotspot bounds, if specified) to a corner. 950 * 951 * @param maxRadius the maximum ripple radius in pixels or 952 * {@link #RADIUS_AUTO} to automatically determine the maximum 953 * radius based on the bounds 954 * @see #getMaxRadius() 955 * @see #setHotspotBounds(int, int, int, int) 956 * @hide 957 */ 958 public void setMaxRadius(int maxRadius) { 959 if (maxRadius != RADIUS_AUTO && maxRadius < 0) { 960 throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0"); 961 } 962 963 mState.mMaxRadius = maxRadius; 964 } 965 966 /** 967 * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if 968 * the radius is determined automatically 969 * @see #setMaxRadius(int) 970 * @hide 971 */ 972 public int getMaxRadius() { 973 return mState.mMaxRadius; 974 } 975 976 private RippleDrawable(RippleState state, Resources res) { 977 mState = new RippleState(state, this, res); 978 mLayerState = mState; 979 980 if (mState.mNum > 0) { 981 ensurePadding(); 982 } 983 984 if (res != null) { 985 mDensity = res.getDisplayMetrics().density; 986 } 987 988 initializeFromState(); 989 } 990 991 private void initializeFromState() { 992 // Initialize from constant state. 993 mMask = findDrawableByLayerId(R.id.mask); 994 } 995} 996