RippleDrawable.java revision 34a14f967ab6c88829c9a36ce6e909c47b3ee398
1/* 2 * Copyright (C) 2013 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 android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.content.res.ColorStateList; 22import android.content.res.Resources; 23import android.content.res.Resources.Theme; 24import android.content.res.TypedArray; 25import android.graphics.Canvas; 26import android.graphics.Color; 27import android.graphics.ColorFilter; 28import android.graphics.Outline; 29import android.graphics.Paint; 30import android.graphics.PixelFormat; 31import android.graphics.PorterDuff.Mode; 32import android.graphics.PorterDuffXfermode; 33import android.graphics.Rect; 34import android.util.AttributeSet; 35import android.util.DisplayMetrics; 36import android.util.Log; 37 38import com.android.internal.R; 39 40import org.xmlpull.v1.XmlPullParser; 41import org.xmlpull.v1.XmlPullParserException; 42 43import java.io.IOException; 44 45/** 46 * Drawable that shows a ripple effect in response to state changes. The 47 * anchoring position of the ripple for a given state may be specified by 48 * calling {@link #setHotspot(float, float)} with the corresponding state 49 * attribute identifier. 50 * <p> 51 * A touch feedback drawable may contain multiple child layers, including a 52 * special mask layer that is not drawn to the screen. A single layer may be set 53 * as the mask by specifying its android:id value as {@link android.R.id#mask}. 54 * <pre> 55 * <code><!-- A red ripple masked against an opaque rectangle. --/> 56 * <ripple android:color="#ffff0000"> 57 * <item android:id="@android:id/mask" 58 * android:drawable="@android:color/white" /> 59 * <ripple /></code> 60 * </pre> 61 * <p> 62 * If a mask layer is set, the ripple effect will be masked against that layer 63 * before it is drawn over the composite of the remaining child layers. 64 * <p> 65 * If no mask layer is set, the ripple effect is masked against the composite 66 * of the child layers. 67 * <pre> 68 * <code><!-- A blue ripple drawn atop a black rectangle. --/> 69 * <ripple android:color="#ff00ff00"> 70 * <item android:drawable="@android:color/black" /> 71 * <ripple /> 72 * 73 * <!-- A red ripple drawn atop a drawable resource. --/> 74 * <ripple android:color="#ff00ff00"> 75 * <item android:drawable="@drawable/my_drawable" /> 76 * <ripple /></code> 77 * </pre> 78 * <p> 79 * If no child layers or mask is specified and the ripple is set as a View 80 * background, the ripple will be drawn atop the first available parent 81 * background within the View's hierarchy. In this case, the drawing region 82 * may extend outside of the Drawable bounds. 83 * <pre> 84 * <code><!-- An unbounded green ripple. --/> 85 * <ripple android:color="#ff0000ff" /></code> 86 * </pre> 87 * 88 * @attr ref android.R.styleable#RippleDrawable_color 89 */ 90public class RippleDrawable extends LayerDrawable { 91 private static final String LOG_TAG = RippleDrawable.class.getSimpleName(); 92 private static final PorterDuffXfermode DST_IN = new PorterDuffXfermode(Mode.DST_IN); 93 private static final PorterDuffXfermode SRC_ATOP = new PorterDuffXfermode(Mode.SRC_ATOP); 94 private static final PorterDuffXfermode SRC_OVER = new PorterDuffXfermode(Mode.SRC_OVER); 95 96 /** 97 * Constant for automatically determining the maximum ripple radius. 98 * 99 * @see #setMaxRadius(int) 100 * @hide 101 */ 102 public static final int RADIUS_AUTO = -1; 103 104 /** The maximum number of ripples supported. */ 105 private static final int MAX_RIPPLES = 10; 106 107 private final Rect mTempRect = new Rect(); 108 109 /** Current ripple effect bounds, used to constrain ripple effects. */ 110 private final Rect mHotspotBounds = new Rect(); 111 112 /** Current drawing bounds, used to compute dirty region. */ 113 private final Rect mDrawingBounds = new Rect(); 114 115 /** Current dirty bounds, union of current and previous drawing bounds. */ 116 private final Rect mDirtyBounds = new Rect(); 117 118 private final RippleState mState; 119 120 /** The masking layer, e.g. the layer with id R.id.mask. */ 121 private Drawable mMask; 122 123 /** The current background. May be actively animating or pending entry. */ 124 private RippleBackground mBackground; 125 126 /** Whether we expect to draw a background when visible. */ 127 private boolean mBackgroundActive; 128 129 /** The current ripple. May be actively animating or pending entry. */ 130 private Ripple mRipple; 131 132 /** Whether we expect to draw a ripple when visible. */ 133 private boolean mRippleActive; 134 135 // Hotspot coordinates that are awaiting activation. 136 private float mPendingX; 137 private float mPendingY; 138 private boolean mHasPending; 139 140 /** 141 * Lazily-created array of actively animating ripples. Inactive ripples are 142 * pruned during draw(). The locations of these will not change. 143 */ 144 private Ripple[] mAnimatingRipples; 145 private int mAnimatingRipplesCount = 0; 146 147 /** Paint used to control appearance of ripples. */ 148 private Paint mRipplePaint; 149 150 /** Paint used to control reveal layer masking. */ 151 private Paint mMaskingPaint; 152 153 /** Target density of the display into which ripples are drawn. */ 154 private float mDensity = 1.0f; 155 156 /** Whether bounds are being overridden. */ 157 private boolean mOverrideBounds; 158 159 /** 160 * Whether hotspots are being cleared. Used to prevent re-entry by 161 * animation finish listeners. 162 */ 163 private boolean mClearingHotspots; 164 165 /** 166 * Constructor used for drawable inflation. 167 */ 168 RippleDrawable() { 169 this(new RippleState(null, null, null), null, null); 170 } 171 172 /** 173 * Creates a new ripple drawable with the specified ripple color and 174 * optional content and mask drawables. 175 * 176 * @param color The ripple color 177 * @param content The content drawable, may be {@code null} 178 * @param mask The mask drawable, may be {@code null} 179 */ 180 public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content, 181 @Nullable Drawable mask) { 182 this(new RippleState(null, null, null), null, null); 183 184 if (color == null) { 185 throw new IllegalArgumentException("RippleDrawable requires a non-null color"); 186 } 187 188 if (content != null) { 189 addLayer(content, null, 0, 0, 0, 0, 0); 190 } 191 192 if (mask != null) { 193 addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0); 194 } 195 196 setColor(color); 197 ensurePadding(); 198 initializeFromState(); 199 } 200 201 @Override 202 public void setAlpha(int alpha) { 203 super.setAlpha(alpha); 204 205 // TODO: Should we support this? 206 } 207 208 @Override 209 public void setColorFilter(ColorFilter cf) { 210 super.setColorFilter(cf); 211 212 // TODO: Should we support this? 213 } 214 215 @Override 216 public int getOpacity() { 217 // Worst-case scenario. 218 return PixelFormat.TRANSLUCENT; 219 } 220 221 @Override 222 protected boolean onStateChange(int[] stateSet) { 223 super.onStateChange(stateSet); 224 225 boolean enabled = false; 226 boolean pressed = false; 227 boolean focused = false; 228 229 final int N = stateSet.length; 230 for (int i = 0; i < N; i++) { 231 if (stateSet[i] == R.attr.state_enabled) { 232 enabled = true; 233 } 234 if (stateSet[i] == R.attr.state_focused) { 235 focused = true; 236 } 237 if (stateSet[i] == R.attr.state_pressed) { 238 pressed = true; 239 } 240 } 241 242 setRippleActive(enabled && pressed); 243 setBackgroundActive(focused || (enabled && pressed)); 244 245 // Update the paint color. Only applicable when animated in software. 246 if (mRipplePaint != null && mState.mColor != null) { 247 final ColorStateList stateList = mState.mColor; 248 final int newColor = stateList.getColorForState(stateSet, 0); 249 final int oldColor = mRipplePaint.getColor(); 250 if (oldColor != newColor) { 251 mRipplePaint.setColor(newColor); 252 invalidateSelf(); 253 return true; 254 } 255 } 256 257 return false; 258 } 259 260 private void setRippleActive(boolean active) { 261 if (mRippleActive != active) { 262 mRippleActive = active; 263 if (active) { 264 activateRipple(); 265 } else { 266 removeRipple(); 267 } 268 } 269 } 270 271 private void setBackgroundActive(boolean active) { 272 if (mBackgroundActive != active) { 273 mBackgroundActive = active; 274 if (active) { 275 activateBackground(); 276 } else { 277 removeBackground(); 278 } 279 } 280 } 281 282 @Override 283 protected void onBoundsChange(Rect bounds) { 284 super.onBoundsChange(bounds); 285 286 if (!mOverrideBounds) { 287 mHotspotBounds.set(bounds); 288 onHotspotBoundsChanged(); 289 } 290 291 invalidateSelf(); 292 } 293 294 @Override 295 public boolean setVisible(boolean visible, boolean restart) { 296 final boolean changed = super.setVisible(visible, restart); 297 298 if (!visible) { 299 clearHotspots(); 300 } else if (changed) { 301 // If we just became visible, ensure the background and ripple 302 // visibilities are consistent with their internal states. 303 if (mRippleActive) { 304 activateRipple(); 305 } 306 307 if (mBackgroundActive) { 308 activateBackground(); 309 } 310 } 311 312 return changed; 313 } 314 315 /** 316 * @hide 317 */ 318 @Override 319 public boolean isProjected() { 320 return getNumberOfLayers() == 0; 321 } 322 323 @Override 324 public boolean isStateful() { 325 return true; 326 } 327 328 public void setColor(ColorStateList color) { 329 mState.mColor = color; 330 invalidateSelf(); 331 } 332 333 @Override 334 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 335 throws XmlPullParserException, IOException { 336 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); 337 updateStateFromTypedArray(a); 338 a.recycle(); 339 340 // Force padding default to STACK before inflating. 341 setPaddingMode(PADDING_MODE_STACK); 342 343 super.inflate(r, parser, attrs, theme); 344 345 setTargetDensity(r.getDisplayMetrics()); 346 initializeFromState(); 347 } 348 349 @Override 350 public boolean setDrawableByLayerId(int id, Drawable drawable) { 351 if (super.setDrawableByLayerId(id, drawable)) { 352 if (id == R.id.mask) { 353 mMask = drawable; 354 } 355 356 return true; 357 } 358 359 return false; 360 } 361 362 /** 363 * Specifies how layer padding should affect the bounds of subsequent 364 * layers. The default and recommended value for RippleDrawable is 365 * {@link #PADDING_MODE_STACK}. 366 * 367 * @param mode padding mode, one of: 368 * <ul> 369 * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the 370 * padding of the previous layer 371 * <li>{@link #PADDING_MODE_STACK} to stack each layer directly 372 * atop the previous layer 373 * </ul> 374 * @see #getPaddingMode() 375 */ 376 @Override 377 public void setPaddingMode(int mode) { 378 super.setPaddingMode(mode); 379 } 380 381 /** 382 * Initializes the constant state from the values in the typed array. 383 */ 384 private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { 385 final RippleState state = mState; 386 387 // Account for any configuration changes. 388 state.mChangingConfigurations |= a.getChangingConfigurations(); 389 390 // Extract the theme attributes, if any. 391 state.mTouchThemeAttrs = a.extractThemeAttrs(); 392 393 final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color); 394 if (color != null) { 395 mState.mColor = color; 396 } 397 398 verifyRequiredAttributes(a); 399 } 400 401 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 402 if (mState.mColor == null && (mState.mTouchThemeAttrs == null 403 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) { 404 throw new XmlPullParserException(a.getPositionDescription() + 405 ": <ripple> requires a valid color attribute"); 406 } 407 } 408 409 /** 410 * Set the density at which this drawable will be rendered. 411 * 412 * @param metrics The display metrics for this drawable. 413 */ 414 private void setTargetDensity(DisplayMetrics metrics) { 415 if (mDensity != metrics.density) { 416 mDensity = metrics.density; 417 invalidateSelf(); 418 } 419 } 420 421 @Override 422 public void applyTheme(Theme t) { 423 super.applyTheme(t); 424 425 final RippleState state = mState; 426 if (state == null || state.mTouchThemeAttrs == null) { 427 return; 428 } 429 430 final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, 431 R.styleable.RippleDrawable); 432 try { 433 updateStateFromTypedArray(a); 434 } catch (XmlPullParserException e) { 435 throw new RuntimeException(e); 436 } finally { 437 a.recycle(); 438 } 439 440 initializeFromState(); 441 } 442 443 @Override 444 public boolean canApplyTheme() { 445 return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null; 446 } 447 448 @Override 449 public void setHotspot(float x, float y) { 450 if (mRipple == null || mBackground == null) { 451 mPendingX = x; 452 mPendingY = y; 453 mHasPending = true; 454 } 455 456 if (mRipple != null) { 457 mRipple.move(x, y); 458 } 459 460 if (mBackground != null) { 461 mBackground.move(x, y); 462 } 463 } 464 465 /** 466 * Creates an active hotspot at the specified location. 467 */ 468 private void activateBackground() { 469 if (mBackground == null) { 470 final float x; 471 final float y; 472 if (mHasPending) { 473 mHasPending = false; 474 x = mPendingX; 475 y = mPendingY; 476 } else { 477 x = mHotspotBounds.exactCenterX(); 478 y = mHotspotBounds.exactCenterY(); 479 } 480 mBackground = new RippleBackground(this, mHotspotBounds, x, y); 481 } 482 483 final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 484 mBackground.setup(mState.mMaxRadius, color, mDensity); 485 mBackground.enter(); 486 } 487 488 private void removeBackground() { 489 if (mBackground != null) { 490 // Don't null out the background, we need it to draw! 491 mBackground.exit(); 492 } 493 } 494 495 /** 496 * Creates an active hotspot at the specified location. 497 */ 498 private void activateRipple() { 499 if (mAnimatingRipplesCount >= MAX_RIPPLES) { 500 // This should never happen unless the user is tapping like a maniac 501 // or there is a bug that's preventing ripples from being removed. 502 return; 503 } 504 505 if (mRipple == null) { 506 final float x; 507 final float y; 508 if (mHasPending) { 509 mHasPending = false; 510 x = mPendingX; 511 y = mPendingY; 512 } else { 513 x = mHotspotBounds.exactCenterX(); 514 y = mHotspotBounds.exactCenterY(); 515 } 516 mRipple = new Ripple(this, mHotspotBounds, x, y); 517 } 518 519 final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 520 mRipple.setup(mState.mMaxRadius, color, mDensity); 521 mRipple.enter(); 522 523 if (mAnimatingRipples == null) { 524 mAnimatingRipples = new Ripple[MAX_RIPPLES]; 525 } 526 mAnimatingRipples[mAnimatingRipplesCount++] = mRipple; 527 } 528 529 private void removeRipple() { 530 if (mRipple != null) { 531 mRipple.exit(); 532 mRipple = null; 533 } 534 } 535 536 private void clearHotspots() { 537 mClearingHotspots = true; 538 539 final int count = mAnimatingRipplesCount; 540 final Ripple[] ripples = mAnimatingRipples; 541 for (int i = 0; i < count; i++) { 542 // Calling cancel may remove the ripple from the animating ripple 543 // array, so cache the reference before nulling it out. 544 final Ripple ripple = ripples[i]; 545 ripples[i] = null; 546 ripple.cancel(); 547 } 548 549 if (mRipple != null) { 550 mRipple.cancel(); 551 mRipple = null; 552 } 553 554 if (mBackground != null) { 555 mBackground.cancel(); 556 mBackground = null; 557 } 558 559 mClearingHotspots = false; 560 mAnimatingRipplesCount = 0; 561 invalidateSelf(); 562 } 563 564 @Override 565 public void setHotspotBounds(int left, int top, int right, int bottom) { 566 mOverrideBounds = true; 567 mHotspotBounds.set(left, top, right, bottom); 568 569 onHotspotBoundsChanged(); 570 } 571 572 /** @hide */ 573 @Override 574 public void getHotspotBounds(Rect outRect) { 575 outRect.set(mHotspotBounds); 576 } 577 578 /** 579 * Notifies all the animating ripples that the hotspot bounds have changed. 580 */ 581 private void onHotspotBoundsChanged() { 582 final int count = mAnimatingRipplesCount; 583 final Ripple[] ripples = mAnimatingRipples; 584 for (int i = 0; i < count; i++) { 585 ripples[i].onHotspotBoundsChanged(); 586 } 587 588 if (mBackground != null) { 589 mBackground.onHotspotBoundsChanged(); 590 } 591 } 592 593 /** 594 * Populates <code>outline</code> with the first available layer outline, 595 * excluding the mask layer. 596 * 597 * @param outline Outline in which to place the first available layer outline 598 */ 599 @Override 600 public void getOutline(@NonNull Outline outline) { 601 final LayerState state = mLayerState; 602 final ChildDrawable[] children = state.mChildren; 603 final int N = state.mNum; 604 for (int i = 0; i < N; i++) { 605 if (children[i].mId != R.id.mask) { 606 children[i].mDrawable.getOutline(outline); 607 if (!outline.isEmpty()) return; 608 } 609 } 610 } 611 612 @Override 613 public void draw(@NonNull Canvas canvas) { 614 final boolean isProjected = isProjected(); 615 final boolean hasMask = mMask != null; 616 final boolean drawNonMaskContent = mLayerState.mNum > (hasMask ? 1 : 0); 617 final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE; 618 final Rect bounds = isProjected ? getDirtyBounds() : getBounds(); 619 620 // If we have content, draw it into a layer first. 621 final int contentLayer = drawNonMaskContent ? 622 drawContentLayer(canvas, bounds, SRC_OVER) : -1; 623 624 // Next, try to draw the ripples (into a layer if necessary). If we need 625 // to mask against the underlying content, set the xfermode to SRC_ATOP. 626 final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP; 627 628 // If we have a background and a non-opaque mask, draw the masking layer. 629 final int backgroundLayer = drawBackgroundLayer(canvas, bounds, xfermode); 630 if (backgroundLayer >= 0) { 631 if (drawMask) { 632 drawMaskingLayer(canvas, bounds, DST_IN); 633 } 634 canvas.restoreToCount(backgroundLayer); 635 } 636 637 // If we have ripples and a non-opaque mask, draw the masking layer. 638 final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode); 639 if (rippleLayer >= 0) { 640 if (drawMask) { 641 drawMaskingLayer(canvas, bounds, DST_IN); 642 } 643 canvas.restoreToCount(rippleLayer); 644 } 645 646 // Composite the layers if needed. 647 if (contentLayer >= 0) { 648 canvas.restoreToCount(contentLayer); 649 } 650 } 651 652 /** 653 * Removes a ripple from the animating ripple list. 654 * 655 * @param ripple the ripple to remove 656 */ 657 void removeRipple(Ripple ripple) { 658 if (!mClearingHotspots) { 659 // Ripple ripple ripple ripple. Ripple ripple. 660 final Ripple[] ripples = mAnimatingRipples; 661 final int count = mAnimatingRipplesCount; 662 final int index = getRippleIndex(ripple); 663 if (index >= 0) { 664 System.arraycopy(ripples, index + 1, ripples, index + 1 - 1, count - (index + 1)); 665 ripples[count - 1] = null; 666 mAnimatingRipplesCount--; 667 invalidateSelf(); 668 } 669 } 670 } 671 672 void removeBackground(RippleBackground background) { 673 if (mBackground == background) { 674 mBackground = null; 675 invalidateSelf(); 676 } 677 } 678 679 private int getRippleIndex(Ripple ripple) { 680 final Ripple[] ripples = mAnimatingRipples; 681 final int count = mAnimatingRipplesCount; 682 for (int i = 0; i < count; i++) { 683 if (ripples[i] == ripple) { 684 return i; 685 } 686 } 687 return -1; 688 } 689 690 private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 691 final ChildDrawable[] array = mLayerState.mChildren; 692 final int count = mLayerState.mNum; 693 694 // We don't need a layer if we don't expect to draw any ripples, we have 695 // an explicit mask, or if the non-mask content is all opaque. 696 boolean needsLayer = false; 697 if ((mAnimatingRipplesCount > 0 || mBackground != null) && mMask == null) { 698 for (int i = 0; i < count; i++) { 699 if (array[i].mId != R.id.mask 700 && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) { 701 needsLayer = true; 702 break; 703 } 704 } 705 } 706 707 final Paint maskingPaint = getMaskingPaint(mode); 708 final int restoreToCount = needsLayer ? canvas.saveLayer(bounds.left, bounds.top, 709 bounds.right, bounds.bottom, maskingPaint) : -1; 710 711 // Draw everything except the mask. 712 for (int i = 0; i < count; i++) { 713 if (array[i].mId != R.id.mask) { 714 array[i].mDrawable.draw(canvas); 715 } 716 } 717 718 return restoreToCount; 719 } 720 721 private int drawBackgroundLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 722 // Separate the ripple color and alpha channel. The alpha will be 723 // applied when we merge the ripples down to the canvas. 724 final int rippleARGB; 725 if (mState.mColor != null) { 726 rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 727 } else { 728 rippleARGB = Color.TRANSPARENT; 729 } 730 731 if (mRipplePaint == null) { 732 mRipplePaint = new Paint(); 733 mRipplePaint.setAntiAlias(true); 734 } 735 736 final int rippleAlpha = Color.alpha(rippleARGB); 737 final Paint ripplePaint = mRipplePaint; 738 ripplePaint.setColor(rippleARGB); 739 ripplePaint.setAlpha(0xFF); 740 741 boolean drewRipples = false; 742 int restoreToCount = -1; 743 int restoreTranslate = -1; 744 745 // Draw background. 746 final RippleBackground background = mBackground; 747 if (background != null) { 748 // If we're masking the ripple layer, make sure we have a layer 749 // first. This will merge SRC_OVER (directly) onto the canvas. 750 final Paint maskingPaint = getMaskingPaint(mode); 751 maskingPaint.setAlpha(rippleAlpha); 752 restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 753 bounds.right, bounds.bottom, maskingPaint); 754 755 restoreTranslate = canvas.save(); 756 // Translate the canvas to the current hotspot bounds. 757 canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY()); 758 759 drewRipples = background.draw(canvas, ripplePaint); 760 } 761 762 // Always restore the translation. 763 if (restoreTranslate >= 0) { 764 canvas.restoreToCount(restoreTranslate); 765 } 766 767 // If we created a layer with no content, merge it immediately. 768 if (restoreToCount >= 0 && !drewRipples) { 769 canvas.restoreToCount(restoreToCount); 770 restoreToCount = -1; 771 } 772 773 return restoreToCount; 774 } 775 776 private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 777 // Separate the ripple color and alpha channel. The alpha will be 778 // applied when we merge the ripples down to the canvas. 779 final int rippleARGB; 780 if (mState.mColor != null) { 781 rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 782 } else { 783 rippleARGB = Color.TRANSPARENT; 784 } 785 786 if (mRipplePaint == null) { 787 mRipplePaint = new Paint(); 788 mRipplePaint.setAntiAlias(true); 789 } 790 791 final int rippleAlpha = Color.alpha(rippleARGB); 792 final Paint ripplePaint = mRipplePaint; 793 ripplePaint.setColor(rippleARGB); 794 ripplePaint.setAlpha(0xFF); 795 796 boolean drewRipples = false; 797 int restoreToCount = -1; 798 int restoreTranslate = -1; 799 800 // Draw ripples and update the animating ripples array. 801 final int count = mAnimatingRipplesCount; 802 final Ripple[] ripples = mAnimatingRipples; 803 for (int i = 0; i < count; i++) { 804 final Ripple ripple = ripples[i]; 805 806 // If we're masking the ripple layer, make sure we have a layer 807 // first. This will merge SRC_OVER (directly) onto the canvas. 808 if (restoreToCount < 0) { 809 final Paint maskingPaint = getMaskingPaint(mode); 810 maskingPaint.setAlpha(rippleAlpha); 811 restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 812 bounds.right, bounds.bottom, maskingPaint); 813 814 restoreTranslate = canvas.save(); 815 // Translate the canvas to the current hotspot bounds. 816 canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY()); 817 } 818 819 drewRipples |= ripple.draw(canvas, ripplePaint); 820 } 821 822 // Always restore the translation. 823 if (restoreTranslate >= 0) { 824 canvas.restoreToCount(restoreTranslate); 825 } 826 827 // If we created a layer with no content, merge it immediately. 828 if (restoreToCount >= 0 && !drewRipples) { 829 canvas.restoreToCount(restoreToCount); 830 restoreToCount = -1; 831 } 832 833 return restoreToCount; 834 } 835 836 private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 837 final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 838 bounds.right, bounds.bottom, getMaskingPaint(mode)); 839 840 // Ensure that DST_IN blends using the entire layer. 841 canvas.drawColor(Color.TRANSPARENT); 842 843 mMask.draw(canvas); 844 845 return restoreToCount; 846 } 847 848 private Paint getMaskingPaint(PorterDuffXfermode xfermode) { 849 if (mMaskingPaint == null) { 850 mMaskingPaint = new Paint(); 851 } 852 mMaskingPaint.setXfermode(xfermode); 853 mMaskingPaint.setAlpha(0xFF); 854 return mMaskingPaint; 855 } 856 857 @Override 858 public Rect getDirtyBounds() { 859 if (isProjected()) { 860 final Rect drawingBounds = mDrawingBounds; 861 final Rect dirtyBounds = mDirtyBounds; 862 dirtyBounds.set(drawingBounds); 863 drawingBounds.setEmpty(); 864 865 final int cX = (int) mHotspotBounds.exactCenterX(); 866 final int cY = (int) mHotspotBounds.exactCenterY(); 867 final Rect rippleBounds = mTempRect; 868 final Ripple[] activeRipples = mAnimatingRipples; 869 final int N = mAnimatingRipplesCount; 870 for (int i = 0; i < N; i++) { 871 activeRipples[i].getBounds(rippleBounds); 872 rippleBounds.offset(cX, cY); 873 drawingBounds.union(rippleBounds); 874 } 875 876 final RippleBackground background = mBackground; 877 if (background != null) { 878 background.getBounds(rippleBounds); 879 rippleBounds.offset(cX, cY); 880 drawingBounds.union(rippleBounds); 881 } 882 883 dirtyBounds.union(drawingBounds); 884 dirtyBounds.union(super.getDirtyBounds()); 885 return dirtyBounds; 886 } else { 887 return getBounds(); 888 } 889 } 890 891 @Override 892 public ConstantState getConstantState() { 893 return mState; 894 } 895 896 static class RippleState extends LayerState { 897 int[] mTouchThemeAttrs; 898 ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); 899 int mMaxRadius = RADIUS_AUTO; 900 901 public RippleState(RippleState orig, RippleDrawable owner, Resources res) { 902 super(orig, owner, res); 903 904 if (orig != null) { 905 mTouchThemeAttrs = orig.mTouchThemeAttrs; 906 mColor = orig.mColor; 907 mMaxRadius = orig.mMaxRadius; 908 } 909 } 910 911 @Override 912 public boolean canApplyTheme() { 913 return mTouchThemeAttrs != null || super.canApplyTheme(); 914 } 915 916 @Override 917 public Drawable newDrawable() { 918 return new RippleDrawable(this, null, null); 919 } 920 921 @Override 922 public Drawable newDrawable(Resources res) { 923 return new RippleDrawable(this, res, null); 924 } 925 926 @Override 927 public Drawable newDrawable(Resources res, Theme theme) { 928 return new RippleDrawable(this, res, theme); 929 } 930 } 931 932 /** 933 * Sets the maximum ripple radius in pixels. The default value of 934 * {@link #RADIUS_AUTO} defines the radius as the distance from the center 935 * of the drawable bounds (or hotspot bounds, if specified) to a corner. 936 * 937 * @param maxRadius the maximum ripple radius in pixels or 938 * {@link #RADIUS_AUTO} to automatically determine the maximum 939 * radius based on the bounds 940 * @see #getMaxRadius() 941 * @see #setHotspotBounds(int, int, int, int) 942 * @hide 943 */ 944 public void setMaxRadius(int maxRadius) { 945 if (maxRadius != RADIUS_AUTO && maxRadius < 0) { 946 throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0"); 947 } 948 949 mState.mMaxRadius = maxRadius; 950 } 951 952 /** 953 * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if 954 * the radius is determined automatically 955 * @see #setMaxRadius(int) 956 * @hide 957 */ 958 public int getMaxRadius() { 959 return mState.mMaxRadius; 960 } 961 962 private RippleDrawable(RippleState state, Resources res, Theme theme) { 963 boolean needsTheme = false; 964 965 final RippleState ns; 966 if (theme != null && state != null && state.canApplyTheme()) { 967 ns = new RippleState(state, this, res); 968 needsTheme = true; 969 } else if (state == null) { 970 ns = new RippleState(null, this, res); 971 } else { 972 // We always need a new state since child drawables contain local 973 // state but live within the parent's constant state. 974 // TODO: Move child drawables into local state. 975 ns = new RippleState(state, this, res); 976 } 977 978 if (res != null) { 979 mDensity = res.getDisplayMetrics().density; 980 } 981 982 mState = ns; 983 mLayerState = ns; 984 985 if (ns.mNum > 0) { 986 ensurePadding(); 987 } 988 989 if (needsTheme) { 990 applyTheme(theme); 991 } 992 993 initializeFromState(); 994 } 995 996 private void initializeFromState() { 997 // Initialize from constant state. 998 mMask = findDrawableByLayerId(R.id.mask); 999 } 1000} 1001