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