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