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