RippleDrawable.java revision 45c4bbbbce6bbad50a033efcba7948a23f1f117a
1/* 2 * Copyright (C) 2014 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 updateLocalState(); 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 /** 356 * Sets the ripple color. 357 * 358 * @param color Ripple color as a color state list. 359 */ 360 public void setColor(ColorStateList color) { 361 mState.mColor = color; 362 invalidateSelf(); 363 } 364 365 @Override 366 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 367 throws XmlPullParserException, IOException { 368 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); 369 updateStateFromTypedArray(a); 370 a.recycle(); 371 372 // Force padding default to STACK before inflating. 373 setPaddingMode(PADDING_MODE_STACK); 374 375 super.inflate(r, parser, attrs, theme); 376 377 setTargetDensity(r.getDisplayMetrics()); 378 379 updateLocalState(); 380 } 381 382 @Override 383 public boolean setDrawableByLayerId(int id, Drawable drawable) { 384 if (super.setDrawableByLayerId(id, drawable)) { 385 if (id == R.id.mask) { 386 mMask = drawable; 387 } 388 389 return true; 390 } 391 392 return false; 393 } 394 395 /** 396 * Specifies how layer padding should affect the bounds of subsequent 397 * layers. The default and recommended value for RippleDrawable is 398 * {@link #PADDING_MODE_STACK}. 399 * 400 * @param mode padding mode, one of: 401 * <ul> 402 * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the 403 * padding of the previous layer 404 * <li>{@link #PADDING_MODE_STACK} to stack each layer directly 405 * atop the previous layer 406 * </ul> 407 * @see #getPaddingMode() 408 */ 409 @Override 410 public void setPaddingMode(int mode) { 411 super.setPaddingMode(mode); 412 } 413 414 /** 415 * Initializes the constant state from the values in the typed array. 416 */ 417 private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { 418 final RippleState state = mState; 419 420 // Account for any configuration changes. 421 state.mChangingConfigurations |= a.getChangingConfigurations(); 422 423 // Extract the theme attributes, if any. 424 state.mTouchThemeAttrs = a.extractThemeAttrs(); 425 426 final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color); 427 if (color != null) { 428 mState.mColor = color; 429 } 430 431 verifyRequiredAttributes(a); 432 } 433 434 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 435 if (mState.mColor == null && (mState.mTouchThemeAttrs == null 436 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) { 437 throw new XmlPullParserException(a.getPositionDescription() + 438 ": <ripple> requires a valid color attribute"); 439 } 440 } 441 442 /** 443 * Set the density at which this drawable will be rendered. 444 * 445 * @param metrics The display metrics for this drawable. 446 */ 447 private void setTargetDensity(DisplayMetrics metrics) { 448 if (mDensity != metrics.density) { 449 mDensity = metrics.density; 450 invalidateSelf(); 451 } 452 } 453 454 @Override 455 public void applyTheme(Theme t) { 456 super.applyTheme(t); 457 458 final RippleState state = mState; 459 if (state == null) { 460 return; 461 } 462 463 if (state.mTouchThemeAttrs != null) { 464 final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, 465 R.styleable.RippleDrawable); 466 try { 467 updateStateFromTypedArray(a); 468 } catch (XmlPullParserException e) { 469 throw new RuntimeException(e); 470 } finally { 471 a.recycle(); 472 } 473 } 474 475 if (state.mColor != null && state.mColor.canApplyTheme()) { 476 state.mColor.applyTheme(t); 477 } 478 479 updateLocalState(); 480 } 481 482 @Override 483 public boolean canApplyTheme() { 484 return (mState != null && mState.canApplyTheme()) || super.canApplyTheme(); 485 } 486 487 @Override 488 public void setHotspot(float x, float y) { 489 if (mRipple == null || mBackground == null) { 490 mPendingX = x; 491 mPendingY = y; 492 mHasPending = true; 493 } 494 495 if (mRipple != null) { 496 mRipple.move(x, y); 497 } 498 } 499 500 /** 501 * Creates an active hotspot at the specified location. 502 */ 503 private void tryBackgroundEnter(boolean focused) { 504 if (mBackground == null) { 505 mBackground = new RippleBackground(this, mHotspotBounds); 506 } 507 508 mBackground.setup(mState.mMaxRadius, mDensity); 509 mBackground.enter(focused); 510 } 511 512 private void tryBackgroundExit() { 513 if (mBackground != null) { 514 // Don't null out the background, we need it to draw! 515 mBackground.exit(); 516 } 517 } 518 519 /** 520 * Attempts to start an enter animation for the active hotspot. Fails if 521 * there are too many animating ripples. 522 */ 523 private void tryRippleEnter() { 524 if (mExitingRipplesCount >= MAX_RIPPLES) { 525 // This should never happen unless the user is tapping like a maniac 526 // or there is a bug that's preventing ripples from being removed. 527 return; 528 } 529 530 if (mRipple == null) { 531 final float x; 532 final float y; 533 if (mHasPending) { 534 mHasPending = false; 535 x = mPendingX; 536 y = mPendingY; 537 } else { 538 x = mHotspotBounds.exactCenterX(); 539 y = mHotspotBounds.exactCenterY(); 540 } 541 mRipple = new Ripple(this, mHotspotBounds, x, y); 542 } 543 544 mRipple.setup(mState.mMaxRadius, mDensity); 545 mRipple.enter(); 546 } 547 548 /** 549 * Attempts to start an exit animation for the active hotspot. Fails if 550 * there is no active hotspot. 551 */ 552 private void tryRippleExit() { 553 if (mRipple != null) { 554 if (mExitingRipples == null) { 555 mExitingRipples = new Ripple[MAX_RIPPLES]; 556 } 557 mExitingRipples[mExitingRipplesCount++] = mRipple; 558 mRipple.exit(); 559 mRipple = null; 560 } 561 } 562 563 /** 564 * Cancels and removes the active ripple, all exiting ripples, and the 565 * background. Nothing will be drawn after this method is called. 566 */ 567 private void clearHotspots() { 568 if (mRipple != null) { 569 mRipple.cancel(); 570 mRipple = null; 571 mRippleActive = false; 572 } 573 574 if (mBackground != null) { 575 mBackground.cancel(); 576 mBackground = null; 577 mBackgroundActive = false; 578 } 579 580 cancelExitingRipples(); 581 invalidateSelf(); 582 } 583 584 @Override 585 public void setHotspotBounds(int left, int top, int right, int bottom) { 586 mOverrideBounds = true; 587 mHotspotBounds.set(left, top, right, bottom); 588 589 onHotspotBoundsChanged(); 590 } 591 592 /** @hide */ 593 @Override 594 public void getHotspotBounds(Rect outRect) { 595 outRect.set(mHotspotBounds); 596 } 597 598 /** 599 * Notifies all the animating ripples that the hotspot bounds have changed. 600 */ 601 private void onHotspotBoundsChanged() { 602 final int count = mExitingRipplesCount; 603 final Ripple[] ripples = mExitingRipples; 604 for (int i = 0; i < count; i++) { 605 ripples[i].onHotspotBoundsChanged(); 606 } 607 608 if (mRipple != null) { 609 mRipple.onHotspotBoundsChanged(); 610 } 611 612 if (mBackground != null) { 613 mBackground.onHotspotBoundsChanged(); 614 } 615 } 616 617 /** 618 * Populates <code>outline</code> with the first available layer outline, 619 * excluding the mask layer. 620 * 621 * @param outline Outline in which to place the first available layer outline 622 */ 623 @Override 624 public void getOutline(@NonNull Outline outline) { 625 final LayerState state = mLayerState; 626 final ChildDrawable[] children = state.mChildren; 627 final int N = state.mNum; 628 for (int i = 0; i < N; i++) { 629 if (children[i].mId != R.id.mask) { 630 children[i].mDrawable.getOutline(outline); 631 if (!outline.isEmpty()) return; 632 } 633 } 634 } 635 636 /** 637 * Optimized for drawing ripples with a mask layer and optional content. 638 */ 639 @Override 640 public void draw(@NonNull Canvas canvas) { 641 // Clip to the dirty bounds, which will be the drawable bounds if we 642 // have a mask or content and the ripple bounds if we're projecting. 643 final Rect bounds = getDirtyBounds(); 644 final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 645 canvas.clipRect(bounds); 646 647 drawContent(canvas); 648 drawBackgroundAndRipples(canvas); 649 650 canvas.restoreToCount(saveCount); 651 } 652 653 @Override 654 public void invalidateSelf() { 655 super.invalidateSelf(); 656 657 // Force the mask to update on the next draw(). 658 mHasValidMask = false; 659 } 660 661 /** 662 * @return whether we need to use a mask 663 */ 664 private void updateMaskShaderIfNeeded() { 665 if (mHasValidMask) { 666 return; 667 } 668 669 final int maskType = getMaskType(); 670 if (maskType == MASK_UNKNOWN) { 671 return; 672 } 673 674 mHasValidMask = true; 675 676 if (maskType == MASK_NONE) { 677 if (mMaskBuffer != null) { 678 mMaskBuffer.recycle(); 679 mMaskBuffer = null; 680 mMaskShader = null; 681 mMaskCanvas = null; 682 } 683 mMaskMatrix = null; 684 mMaskColorFilter = null; 685 return; 686 } 687 688 // Ensure we have a correctly-sized buffer. 689 final Rect bounds = getBounds(); 690 if (mMaskBuffer == null 691 || mMaskBuffer.getWidth() != bounds.width() 692 || mMaskBuffer.getHeight() != bounds.height()) { 693 if (mMaskBuffer != null) { 694 mMaskBuffer.recycle(); 695 } 696 697 mMaskBuffer = Bitmap.createBitmap( 698 bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); 699 mMaskShader = new BitmapShader(mMaskBuffer, 700 Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 701 mMaskCanvas = new Canvas(mMaskBuffer); 702 } else { 703 mMaskBuffer.eraseColor(Color.TRANSPARENT); 704 } 705 706 if (mMaskMatrix == null) { 707 mMaskMatrix = new Matrix(); 708 } else { 709 mMaskMatrix.reset(); 710 } 711 712 if (mMaskColorFilter == null) { 713 mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN); 714 } 715 716 // Draw the appropriate mask. 717 if (maskType == MASK_EXPLICIT) { 718 drawMask(mMaskCanvas); 719 } else if (maskType == MASK_CONTENT) { 720 drawContent(mMaskCanvas); 721 } 722 } 723 724 private int getMaskType() { 725 if (mRipple == null && mExitingRipplesCount <= 0 726 && (mBackground == null || !mBackground.shouldDraw())) { 727 // We might need a mask later. 728 return MASK_UNKNOWN; 729 } 730 731 if (mMask != null) { 732 if (mMask.getOpacity() == PixelFormat.OPAQUE) { 733 // Clipping handles opaque explicit masks. 734 return MASK_NONE; 735 } else { 736 return MASK_EXPLICIT; 737 } 738 } 739 740 // Check for non-opaque, non-mask content. 741 final ChildDrawable[] array = mLayerState.mChildren; 742 final int count = mLayerState.mNum; 743 for (int i = 0; i < count; i++) { 744 if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) { 745 return MASK_CONTENT; 746 } 747 } 748 749 // Clipping handles opaque content. 750 return MASK_NONE; 751 } 752 753 /** 754 * Removes a ripple from the exiting ripple list. 755 * 756 * @param ripple the ripple to remove 757 */ 758 void removeRipple(Ripple ripple) { 759 // Ripple ripple ripple ripple. Ripple ripple. 760 final Ripple[] ripples = mExitingRipples; 761 final int count = mExitingRipplesCount; 762 final int index = getRippleIndex(ripple); 763 if (index >= 0) { 764 System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1)); 765 ripples[count - 1] = null; 766 mExitingRipplesCount--; 767 768 invalidateSelf(); 769 } 770 } 771 772 private int getRippleIndex(Ripple ripple) { 773 final Ripple[] ripples = mExitingRipples; 774 final int count = mExitingRipplesCount; 775 for (int i = 0; i < count; i++) { 776 if (ripples[i] == ripple) { 777 return i; 778 } 779 } 780 return -1; 781 } 782 783 private void drawContent(Canvas canvas) { 784 // Draw everything except the mask. 785 final ChildDrawable[] array = mLayerState.mChildren; 786 final int count = mLayerState.mNum; 787 for (int i = 0; i < count; i++) { 788 if (array[i].mId != R.id.mask) { 789 array[i].mDrawable.draw(canvas); 790 } 791 } 792 } 793 794 private void drawBackgroundAndRipples(Canvas canvas) { 795 final Ripple active = mRipple; 796 final RippleBackground background = mBackground; 797 final int count = mExitingRipplesCount; 798 if (active == null && count <= 0 && (background == null || !background.shouldDraw())) { 799 // Move along, nothing to draw here. 800 return; 801 } 802 803 final float x = mHotspotBounds.exactCenterX(); 804 final float y = mHotspotBounds.exactCenterY(); 805 canvas.translate(x, y); 806 807 updateMaskShaderIfNeeded(); 808 809 // Position the shader to account for canvas translation. 810 if (mMaskShader != null) { 811 mMaskMatrix.setTranslate(-x, -y); 812 mMaskShader.setLocalMatrix(mMaskMatrix); 813 } 814 815 // Grab the color for the current state and cut the alpha channel in 816 // half so that the ripple and background together yield full alpha. 817 final int color = mState.mColor.getColorForState(getState(), Color.BLACK); 818 final int halfAlpha = (Color.alpha(color) / 2) << 24; 819 final Paint p = getRipplePaint(); 820 821 if (mMaskColorFilter != null) { 822 // The ripple timing depends on the paint's alpha value, so we need 823 // to push just the alpha channel into the paint and let the filter 824 // handle the full-alpha color. 825 final int fullAlphaColor = color | (0xFF << 24); 826 mMaskColorFilter.setColor(fullAlphaColor); 827 828 p.setColor(halfAlpha); 829 p.setColorFilter(mMaskColorFilter); 830 p.setShader(mMaskShader); 831 } else { 832 final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha; 833 p.setColor(halfAlphaColor); 834 p.setColorFilter(null); 835 p.setShader(null); 836 } 837 838 if (background != null && background.shouldDraw()) { 839 background.draw(canvas, p); 840 } 841 842 if (count > 0) { 843 final Ripple[] ripples = mExitingRipples; 844 for (int i = 0; i < count; i++) { 845 ripples[i].draw(canvas, p); 846 } 847 } 848 849 if (active != null) { 850 active.draw(canvas, p); 851 } 852 853 canvas.translate(-x, -y); 854 } 855 856 private void drawMask(Canvas canvas) { 857 mMask.draw(canvas); 858 } 859 860 private Paint getRipplePaint() { 861 if (mRipplePaint == null) { 862 mRipplePaint = new Paint(); 863 mRipplePaint.setAntiAlias(true); 864 mRipplePaint.setStyle(Paint.Style.FILL); 865 } 866 return mRipplePaint; 867 } 868 869 @Override 870 public Rect getDirtyBounds() { 871 if (isProjected()) { 872 final Rect drawingBounds = mDrawingBounds; 873 final Rect dirtyBounds = mDirtyBounds; 874 dirtyBounds.set(drawingBounds); 875 drawingBounds.setEmpty(); 876 877 final int cX = (int) mHotspotBounds.exactCenterX(); 878 final int cY = (int) mHotspotBounds.exactCenterY(); 879 final Rect rippleBounds = mTempRect; 880 881 final Ripple[] activeRipples = mExitingRipples; 882 final int N = mExitingRipplesCount; 883 for (int i = 0; i < N; i++) { 884 activeRipples[i].getBounds(rippleBounds); 885 rippleBounds.offset(cX, cY); 886 drawingBounds.union(rippleBounds); 887 } 888 889 final RippleBackground background = mBackground; 890 if (background != null) { 891 background.getBounds(rippleBounds); 892 rippleBounds.offset(cX, cY); 893 drawingBounds.union(rippleBounds); 894 } 895 896 dirtyBounds.union(drawingBounds); 897 dirtyBounds.union(super.getDirtyBounds()); 898 return dirtyBounds; 899 } else { 900 return getBounds(); 901 } 902 } 903 904 @Override 905 public ConstantState getConstantState() { 906 return mState; 907 } 908 909 @Override 910 public Drawable mutate() { 911 super.mutate(); 912 913 // LayerDrawable creates a new state using createConstantState, so 914 // this should always be a safe cast. 915 mState = (RippleState) mLayerState; 916 917 // The locally cached drawable may have changed. 918 mMask = findDrawableByLayerId(R.id.mask); 919 920 return this; 921 } 922 923 @Override 924 RippleState createConstantState(LayerState state, Resources res) { 925 return new RippleState(state, this, res); 926 } 927 928 static class RippleState extends LayerState { 929 int[] mTouchThemeAttrs; 930 ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); 931 int mMaxRadius = RADIUS_AUTO; 932 933 public RippleState(LayerState orig, RippleDrawable owner, Resources res) { 934 super(orig, owner, res); 935 936 if (orig != null && orig instanceof RippleState) { 937 final RippleState origs = (RippleState) orig; 938 mTouchThemeAttrs = origs.mTouchThemeAttrs; 939 mColor = origs.mColor; 940 mMaxRadius = origs.mMaxRadius; 941 } 942 } 943 944 @Override 945 public boolean canApplyTheme() { 946 return mTouchThemeAttrs != null 947 || (mColor != null && mColor.canApplyTheme()) 948 || super.canApplyTheme(); 949 } 950 951 @Override 952 public Drawable newDrawable() { 953 return new RippleDrawable(this, null); 954 } 955 956 @Override 957 public Drawable newDrawable(Resources res) { 958 return new RippleDrawable(this, res); 959 } 960 } 961 962 /** 963 * Sets the maximum ripple radius in pixels. The default value of 964 * {@link #RADIUS_AUTO} defines the radius as the distance from the center 965 * of the drawable bounds (or hotspot bounds, if specified) to a corner. 966 * 967 * @param maxRadius the maximum ripple radius in pixels or 968 * {@link #RADIUS_AUTO} to automatically determine the maximum 969 * radius based on the bounds 970 * @see #getMaxRadius() 971 * @see #setHotspotBounds(int, int, int, int) 972 * @hide 973 */ 974 public void setMaxRadius(int maxRadius) { 975 if (maxRadius != RADIUS_AUTO && maxRadius < 0) { 976 throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0"); 977 } 978 979 mState.mMaxRadius = maxRadius; 980 } 981 982 /** 983 * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if 984 * the radius is determined automatically 985 * @see #setMaxRadius(int) 986 * @hide 987 */ 988 public int getMaxRadius() { 989 return mState.mMaxRadius; 990 } 991 992 private RippleDrawable(RippleState state, Resources res) { 993 mState = new RippleState(state, this, res); 994 mLayerState = mState; 995 996 if (mState.mNum > 0) { 997 ensurePadding(); 998 } 999 1000 if (res != null) { 1001 mDensity = res.getDisplayMetrics().density; 1002 } 1003 1004 updateLocalState(); 1005 } 1006 1007 private void updateLocalState() { 1008 // Initialize from constant state. 1009 mMask = findDrawableByLayerId(R.id.mask); 1010 } 1011} 1012