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