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