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