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