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