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