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