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