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