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