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