RippleDrawable.java revision d78a44576c6bac5541e04c1f38599d43c9943653
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 jumpToCurrentState() { 203 super.jumpToCurrentState(); 204 205 if (mRipple != null) { 206 mRipple.jump(); 207 } 208 209 if (mBackground != null) { 210 mBackground.jump(); 211 } 212 213 mClearingHotspots = true; 214 final int count = mAnimatingRipplesCount; 215 final Ripple[] ripples = mAnimatingRipples; 216 for (int i = 0; i < count; i++) { 217 ripples[i].jump(); 218 ripples[i] = null; 219 } 220 mAnimatingRipplesCount = 0; 221 mClearingHotspots = false; 222 } 223 224 @Override 225 public void setAlpha(int alpha) { 226 super.setAlpha(alpha); 227 228 // TODO: Should we support this? 229 } 230 231 @Override 232 public void setColorFilter(ColorFilter cf) { 233 super.setColorFilter(cf); 234 235 // TODO: Should we support this? 236 } 237 238 @Override 239 public int getOpacity() { 240 // Worst-case scenario. 241 return PixelFormat.TRANSLUCENT; 242 } 243 244 @Override 245 protected boolean onStateChange(int[] stateSet) { 246 super.onStateChange(stateSet); 247 248 boolean enabled = false; 249 boolean pressed = false; 250 boolean focused = false; 251 252 final int N = stateSet.length; 253 for (int i = 0; i < N; i++) { 254 if (stateSet[i] == R.attr.state_enabled) { 255 enabled = true; 256 } 257 if (stateSet[i] == R.attr.state_focused) { 258 focused = true; 259 } 260 if (stateSet[i] == R.attr.state_pressed) { 261 pressed = true; 262 } 263 } 264 265 setRippleActive(enabled && pressed); 266 setBackgroundActive(focused || (enabled && pressed)); 267 268 // Update the paint color. Only applicable when animated in software. 269 if (mRipplePaint != null && mState.mColor != null) { 270 final ColorStateList stateList = mState.mColor; 271 final int newColor = stateList.getColorForState(stateSet, 0); 272 final int oldColor = mRipplePaint.getColor(); 273 if (oldColor != newColor) { 274 mRipplePaint.setColor(newColor); 275 invalidateSelf(); 276 return true; 277 } 278 } 279 280 return false; 281 } 282 283 private void setRippleActive(boolean active) { 284 if (mRippleActive != active) { 285 mRippleActive = active; 286 if (active) { 287 activateRipple(); 288 } else { 289 removeRipple(); 290 } 291 } 292 } 293 294 private void setBackgroundActive(boolean active) { 295 if (mBackgroundActive != active) { 296 mBackgroundActive = active; 297 if (active) { 298 activateBackground(); 299 } else { 300 removeBackground(); 301 } 302 } 303 } 304 305 @Override 306 protected void onBoundsChange(Rect bounds) { 307 super.onBoundsChange(bounds); 308 309 if (!mOverrideBounds) { 310 mHotspotBounds.set(bounds); 311 onHotspotBoundsChanged(); 312 } 313 314 invalidateSelf(); 315 } 316 317 @Override 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 activateRipple(); 328 } 329 330 if (mBackgroundActive) { 331 activateBackground(); 332 } 333 } 334 335 return changed; 336 } 337 338 /** 339 * @hide 340 */ 341 @Override 342 public boolean isProjected() { 343 return getNumberOfLayers() == 0; 344 } 345 346 @Override 347 public boolean isStateful() { 348 return true; 349 } 350 351 public void setColor(ColorStateList color) { 352 mState.mColor = color; 353 invalidateSelf(); 354 } 355 356 @Override 357 public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) 358 throws XmlPullParserException, IOException { 359 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); 360 updateStateFromTypedArray(a); 361 a.recycle(); 362 363 // Force padding default to STACK before inflating. 364 setPaddingMode(PADDING_MODE_STACK); 365 366 super.inflate(r, parser, attrs, theme); 367 368 setTargetDensity(r.getDisplayMetrics()); 369 initializeFromState(); 370 } 371 372 @Override 373 public boolean setDrawableByLayerId(int id, Drawable drawable) { 374 if (super.setDrawableByLayerId(id, drawable)) { 375 if (id == R.id.mask) { 376 mMask = drawable; 377 } 378 379 return true; 380 } 381 382 return false; 383 } 384 385 /** 386 * Specifies how layer padding should affect the bounds of subsequent 387 * layers. The default and recommended value for RippleDrawable is 388 * {@link #PADDING_MODE_STACK}. 389 * 390 * @param mode padding mode, one of: 391 * <ul> 392 * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the 393 * padding of the previous layer 394 * <li>{@link #PADDING_MODE_STACK} to stack each layer directly 395 * atop the previous layer 396 * </ul> 397 * @see #getPaddingMode() 398 */ 399 @Override 400 public void setPaddingMode(int mode) { 401 super.setPaddingMode(mode); 402 } 403 404 /** 405 * Initializes the constant state from the values in the typed array. 406 */ 407 private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { 408 final RippleState state = mState; 409 410 // Account for any configuration changes. 411 state.mChangingConfigurations |= a.getChangingConfigurations(); 412 413 // Extract the theme attributes, if any. 414 state.mTouchThemeAttrs = a.extractThemeAttrs(); 415 416 final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color); 417 if (color != null) { 418 mState.mColor = color; 419 } 420 421 verifyRequiredAttributes(a); 422 } 423 424 private void verifyRequiredAttributes(TypedArray a) throws XmlPullParserException { 425 if (mState.mColor == null && (mState.mTouchThemeAttrs == null 426 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) { 427 throw new XmlPullParserException(a.getPositionDescription() + 428 ": <ripple> requires a valid color attribute"); 429 } 430 } 431 432 /** 433 * Set the density at which this drawable will be rendered. 434 * 435 * @param metrics The display metrics for this drawable. 436 */ 437 private void setTargetDensity(DisplayMetrics metrics) { 438 if (mDensity != metrics.density) { 439 mDensity = metrics.density; 440 invalidateSelf(); 441 } 442 } 443 444 @Override 445 public void applyTheme(Theme t) { 446 super.applyTheme(t); 447 448 final RippleState state = mState; 449 if (state == null || state.mTouchThemeAttrs == null) { 450 return; 451 } 452 453 final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, 454 R.styleable.RippleDrawable); 455 try { 456 updateStateFromTypedArray(a); 457 } catch (XmlPullParserException e) { 458 throw new RuntimeException(e); 459 } finally { 460 a.recycle(); 461 } 462 463 initializeFromState(); 464 } 465 466 @Override 467 public boolean canApplyTheme() { 468 return super.canApplyTheme() || mState != null && mState.mTouchThemeAttrs != null; 469 } 470 471 @Override 472 public void setHotspot(float x, float y) { 473 if (mRipple == null || mBackground == null) { 474 mPendingX = x; 475 mPendingY = y; 476 mHasPending = true; 477 } 478 479 if (mRipple != null) { 480 mRipple.move(x, y); 481 } 482 483 if (mBackground != null) { 484 mBackground.move(x, y); 485 } 486 } 487 488 /** 489 * Creates an active hotspot at the specified location. 490 */ 491 private void activateBackground() { 492 if (mBackground == null) { 493 final float x; 494 final float y; 495 if (mHasPending) { 496 mHasPending = false; 497 x = mPendingX; 498 y = mPendingY; 499 } else { 500 x = mHotspotBounds.exactCenterX(); 501 y = mHotspotBounds.exactCenterY(); 502 } 503 mBackground = new RippleBackground(this, mHotspotBounds, x, y); 504 } 505 506 final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 507 mBackground.setup(mState.mMaxRadius, color, mDensity); 508 mBackground.enter(); 509 } 510 511 private void removeBackground() { 512 if (mBackground != null) { 513 // Don't null out the background, we need it to draw! 514 mBackground.exit(); 515 } 516 } 517 518 /** 519 * Creates an active hotspot at the specified location. 520 */ 521 private void activateRipple() { 522 if (mAnimatingRipplesCount >= MAX_RIPPLES) { 523 // This should never happen unless the user is tapping like a maniac 524 // or there is a bug that's preventing ripples from being removed. 525 return; 526 } 527 528 if (mRipple == null) { 529 final float x; 530 final float y; 531 if (mHasPending) { 532 mHasPending = false; 533 x = mPendingX; 534 y = mPendingY; 535 } else { 536 x = mHotspotBounds.exactCenterX(); 537 y = mHotspotBounds.exactCenterY(); 538 } 539 mRipple = new Ripple(this, mHotspotBounds, x, y); 540 } 541 542 final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 543 mRipple.setup(mState.mMaxRadius, color, mDensity); 544 mRipple.enter(); 545 546 if (mAnimatingRipples == null) { 547 mAnimatingRipples = new Ripple[MAX_RIPPLES]; 548 } 549 mAnimatingRipples[mAnimatingRipplesCount++] = mRipple; 550 } 551 552 private void removeRipple() { 553 if (mRipple != null) { 554 mRipple.exit(); 555 mRipple = null; 556 } 557 } 558 559 private void clearHotspots() { 560 if (mRipple != null) { 561 mRipple.cancel(); 562 mRipple = null; 563 } 564 565 if (mBackground != null) { 566 mBackground.cancel(); 567 mBackground = null; 568 } 569 570 mClearingHotspots = true; 571 final int count = mAnimatingRipplesCount; 572 final Ripple[] ripples = mAnimatingRipples; 573 for (int i = 0; i < count; i++) { 574 ripples[i].cancel(); 575 ripples[i] = null; 576 } 577 mAnimatingRipplesCount = 0; 578 mClearingHotspots = false; 579 580 invalidateSelf(); 581 } 582 583 @Override 584 public void setHotspotBounds(int left, int top, int right, int bottom) { 585 mOverrideBounds = true; 586 mHotspotBounds.set(left, top, right, bottom); 587 588 onHotspotBoundsChanged(); 589 } 590 591 /** @hide */ 592 @Override 593 public void getHotspotBounds(Rect outRect) { 594 outRect.set(mHotspotBounds); 595 } 596 597 /** 598 * Notifies all the animating ripples that the hotspot bounds have changed. 599 */ 600 private void onHotspotBoundsChanged() { 601 final int count = mAnimatingRipplesCount; 602 final Ripple[] ripples = mAnimatingRipples; 603 for (int i = 0; i < count; i++) { 604 ripples[i].onHotspotBoundsChanged(); 605 } 606 607 if (mBackground != null) { 608 mBackground.onHotspotBoundsChanged(); 609 } 610 } 611 612 /** 613 * Populates <code>outline</code> with the first available layer outline, 614 * excluding the mask layer. 615 * 616 * @param outline Outline in which to place the first available layer outline 617 */ 618 @Override 619 public void getOutline(@NonNull Outline outline) { 620 final LayerState state = mLayerState; 621 final ChildDrawable[] children = state.mChildren; 622 final int N = state.mNum; 623 for (int i = 0; i < N; i++) { 624 if (children[i].mId != R.id.mask) { 625 children[i].mDrawable.getOutline(outline); 626 if (!outline.isEmpty()) return; 627 } 628 } 629 } 630 631 @Override 632 public void draw(@NonNull Canvas canvas) { 633 final boolean isProjected = isProjected(); 634 final boolean hasMask = mMask != null; 635 final boolean drawNonMaskContent = mLayerState.mNum > (hasMask ? 1 : 0); 636 final boolean drawMask = hasMask && mMask.getOpacity() != PixelFormat.OPAQUE; 637 final Rect bounds = isProjected ? getDirtyBounds() : getBounds(); 638 639 // If we have content, draw it into a layer first. 640 final int contentLayer = drawNonMaskContent ? 641 drawContentLayer(canvas, bounds, SRC_OVER) : -1; 642 643 // Next, try to draw the ripples (into a layer if necessary). If we need 644 // to mask against the underlying content, set the xfermode to SRC_ATOP. 645 final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP; 646 647 // If we have a background and a non-opaque mask, draw the masking layer. 648 final int backgroundLayer = drawBackgroundLayer(canvas, bounds, xfermode); 649 if (backgroundLayer >= 0) { 650 if (drawMask) { 651 drawMaskingLayer(canvas, bounds, DST_IN); 652 } 653 canvas.restoreToCount(backgroundLayer); 654 } 655 656 // If we have ripples and a non-opaque mask, draw the masking layer. 657 final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode); 658 if (rippleLayer >= 0) { 659 if (drawMask) { 660 drawMaskingLayer(canvas, bounds, DST_IN); 661 } 662 canvas.restoreToCount(rippleLayer); 663 } 664 665 // Composite the layers if needed. 666 if (contentLayer >= 0) { 667 canvas.restoreToCount(contentLayer); 668 } 669 } 670 671 /** 672 * Removes a ripple from the animating ripple list. 673 * 674 * @param ripple the ripple to remove 675 */ 676 void removeRipple(Ripple ripple) { 677 if (!mClearingHotspots) { 678 // Ripple ripple ripple ripple. Ripple ripple. 679 final Ripple[] ripples = mAnimatingRipples; 680 final int count = mAnimatingRipplesCount; 681 final int index = getRippleIndex(ripple); 682 if (index >= 0) { 683 System.arraycopy(ripples, index + 1, ripples, index + 1 - 1, count - (index + 1)); 684 ripples[count - 1] = null; 685 mAnimatingRipplesCount--; 686 invalidateSelf(); 687 } 688 } 689 } 690 691 void removeBackground(RippleBackground background) { 692 if (mBackground == background) { 693 mBackground = null; 694 invalidateSelf(); 695 } 696 } 697 698 private int getRippleIndex(Ripple ripple) { 699 final Ripple[] ripples = mAnimatingRipples; 700 final int count = mAnimatingRipplesCount; 701 for (int i = 0; i < count; i++) { 702 if (ripples[i] == ripple) { 703 return i; 704 } 705 } 706 return -1; 707 } 708 709 private int drawContentLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 710 final ChildDrawable[] array = mLayerState.mChildren; 711 final int count = mLayerState.mNum; 712 713 // We don't need a layer if we don't expect to draw any ripples, we have 714 // an explicit mask, or if the non-mask content is all opaque. 715 boolean needsLayer = false; 716 if ((mAnimatingRipplesCount > 0 || mBackground != null) && mMask == null) { 717 for (int i = 0; i < count; i++) { 718 if (array[i].mId != R.id.mask 719 && array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) { 720 needsLayer = true; 721 break; 722 } 723 } 724 } 725 726 final Paint maskingPaint = getMaskingPaint(mode); 727 final int restoreToCount = needsLayer ? canvas.saveLayer(bounds.left, bounds.top, 728 bounds.right, bounds.bottom, maskingPaint) : -1; 729 730 // Draw everything except the mask. 731 for (int i = 0; i < count; i++) { 732 if (array[i].mId != R.id.mask) { 733 array[i].mDrawable.draw(canvas); 734 } 735 } 736 737 return restoreToCount; 738 } 739 740 private int drawBackgroundLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 741 // Separate the ripple color and alpha channel. The alpha will be 742 // applied when we merge the ripples down to the canvas. 743 final int rippleARGB; 744 if (mState.mColor != null) { 745 rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 746 } else { 747 rippleARGB = Color.TRANSPARENT; 748 } 749 750 if (mRipplePaint == null) { 751 mRipplePaint = new Paint(); 752 mRipplePaint.setAntiAlias(true); 753 } 754 755 final int rippleAlpha = Color.alpha(rippleARGB); 756 final Paint ripplePaint = mRipplePaint; 757 ripplePaint.setColor(rippleARGB); 758 ripplePaint.setAlpha(0xFF); 759 760 boolean drewRipples = false; 761 int restoreToCount = -1; 762 int restoreTranslate = -1; 763 764 // Draw background. 765 final RippleBackground background = mBackground; 766 if (background != null) { 767 // If we're masking the ripple layer, make sure we have a layer 768 // first. This will merge SRC_OVER (directly) onto the canvas. 769 final Paint maskingPaint = getMaskingPaint(mode); 770 maskingPaint.setAlpha(rippleAlpha); 771 restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 772 bounds.right, bounds.bottom, maskingPaint); 773 774 restoreTranslate = canvas.save(); 775 // Translate the canvas to the current hotspot bounds. 776 canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY()); 777 778 drewRipples = background.draw(canvas, ripplePaint); 779 } 780 781 // Always restore the translation. 782 if (restoreTranslate >= 0) { 783 canvas.restoreToCount(restoreTranslate); 784 } 785 786 // If we created a layer with no content, merge it immediately. 787 if (restoreToCount >= 0 && !drewRipples) { 788 canvas.restoreToCount(restoreToCount); 789 restoreToCount = -1; 790 } 791 792 return restoreToCount; 793 } 794 795 private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 796 // Separate the ripple color and alpha channel. The alpha will be 797 // applied when we merge the ripples down to the canvas. 798 final int rippleARGB; 799 if (mState.mColor != null) { 800 rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT); 801 } else { 802 rippleARGB = Color.TRANSPARENT; 803 } 804 805 if (mRipplePaint == null) { 806 mRipplePaint = new Paint(); 807 mRipplePaint.setAntiAlias(true); 808 } 809 810 final int rippleAlpha = Color.alpha(rippleARGB); 811 final Paint ripplePaint = mRipplePaint; 812 ripplePaint.setColor(rippleARGB); 813 ripplePaint.setAlpha(0xFF); 814 815 boolean drewRipples = false; 816 int restoreToCount = -1; 817 int restoreTranslate = -1; 818 819 // Draw ripples and update the animating ripples array. 820 final int count = mAnimatingRipplesCount; 821 final Ripple[] ripples = mAnimatingRipples; 822 for (int i = 0; i < count; i++) { 823 final Ripple ripple = ripples[i]; 824 825 // If we're masking the ripple layer, make sure we have a layer 826 // first. This will merge SRC_OVER (directly) onto the canvas. 827 if (restoreToCount < 0) { 828 final Paint maskingPaint = getMaskingPaint(mode); 829 maskingPaint.setAlpha(rippleAlpha); 830 restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 831 bounds.right, bounds.bottom, maskingPaint); 832 833 restoreTranslate = canvas.save(); 834 // Translate the canvas to the current hotspot bounds. 835 canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY()); 836 } 837 838 drewRipples |= ripple.draw(canvas, ripplePaint); 839 } 840 841 // Always restore the translation. 842 if (restoreTranslate >= 0) { 843 canvas.restoreToCount(restoreTranslate); 844 } 845 846 // If we created a layer with no content, merge it immediately. 847 if (restoreToCount >= 0 && !drewRipples) { 848 canvas.restoreToCount(restoreToCount); 849 restoreToCount = -1; 850 } 851 852 return restoreToCount; 853 } 854 855 private int drawMaskingLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) { 856 final int restoreToCount = canvas.saveLayer(bounds.left, bounds.top, 857 bounds.right, bounds.bottom, getMaskingPaint(mode)); 858 859 // Ensure that DST_IN blends using the entire layer. 860 canvas.drawColor(Color.TRANSPARENT); 861 862 mMask.draw(canvas); 863 864 return restoreToCount; 865 } 866 867 private Paint getMaskingPaint(PorterDuffXfermode xfermode) { 868 if (mMaskingPaint == null) { 869 mMaskingPaint = new Paint(); 870 } 871 mMaskingPaint.setXfermode(xfermode); 872 mMaskingPaint.setAlpha(0xFF); 873 return mMaskingPaint; 874 } 875 876 @Override 877 public Rect getDirtyBounds() { 878 if (isProjected()) { 879 final Rect drawingBounds = mDrawingBounds; 880 final Rect dirtyBounds = mDirtyBounds; 881 dirtyBounds.set(drawingBounds); 882 drawingBounds.setEmpty(); 883 884 final int cX = (int) mHotspotBounds.exactCenterX(); 885 final int cY = (int) mHotspotBounds.exactCenterY(); 886 final Rect rippleBounds = mTempRect; 887 final Ripple[] activeRipples = mAnimatingRipples; 888 final int N = mAnimatingRipplesCount; 889 for (int i = 0; i < N; i++) { 890 activeRipples[i].getBounds(rippleBounds); 891 rippleBounds.offset(cX, cY); 892 drawingBounds.union(rippleBounds); 893 } 894 895 final RippleBackground background = mBackground; 896 if (background != null) { 897 background.getBounds(rippleBounds); 898 rippleBounds.offset(cX, cY); 899 drawingBounds.union(rippleBounds); 900 } 901 902 dirtyBounds.union(drawingBounds); 903 dirtyBounds.union(super.getDirtyBounds()); 904 return dirtyBounds; 905 } else { 906 return getBounds(); 907 } 908 } 909 910 @Override 911 public ConstantState getConstantState() { 912 return mState; 913 } 914 915 static class RippleState extends LayerState { 916 int[] mTouchThemeAttrs; 917 ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); 918 int mMaxRadius = RADIUS_AUTO; 919 920 public RippleState(RippleState orig, RippleDrawable owner, Resources res) { 921 super(orig, owner, res); 922 923 if (orig != null) { 924 mTouchThemeAttrs = orig.mTouchThemeAttrs; 925 mColor = orig.mColor; 926 mMaxRadius = orig.mMaxRadius; 927 } 928 } 929 930 @Override 931 public boolean canApplyTheme() { 932 return mTouchThemeAttrs != null || super.canApplyTheme(); 933 } 934 935 @Override 936 public Drawable newDrawable() { 937 return new RippleDrawable(this, null, null); 938 } 939 940 @Override 941 public Drawable newDrawable(Resources res) { 942 return new RippleDrawable(this, res, null); 943 } 944 945 @Override 946 public Drawable newDrawable(Resources res, Theme theme) { 947 return new RippleDrawable(this, res, theme); 948 } 949 } 950 951 /** 952 * Sets the maximum ripple radius in pixels. The default value of 953 * {@link #RADIUS_AUTO} defines the radius as the distance from the center 954 * of the drawable bounds (or hotspot bounds, if specified) to a corner. 955 * 956 * @param maxRadius the maximum ripple radius in pixels or 957 * {@link #RADIUS_AUTO} to automatically determine the maximum 958 * radius based on the bounds 959 * @see #getMaxRadius() 960 * @see #setHotspotBounds(int, int, int, int) 961 * @hide 962 */ 963 public void setMaxRadius(int maxRadius) { 964 if (maxRadius != RADIUS_AUTO && maxRadius < 0) { 965 throw new IllegalArgumentException("maxRadius must be RADIUS_AUTO or >= 0"); 966 } 967 968 mState.mMaxRadius = maxRadius; 969 } 970 971 /** 972 * @return the maximum ripple radius in pixels, or {@link #RADIUS_AUTO} if 973 * the radius is determined automatically 974 * @see #setMaxRadius(int) 975 * @hide 976 */ 977 public int getMaxRadius() { 978 return mState.mMaxRadius; 979 } 980 981 private RippleDrawable(RippleState state, Resources res, Theme theme) { 982 boolean needsTheme = false; 983 984 final RippleState ns; 985 if (theme != null && state != null && state.canApplyTheme()) { 986 ns = new RippleState(state, this, res); 987 needsTheme = true; 988 } else if (state == null) { 989 ns = new RippleState(null, this, res); 990 } else { 991 // We always need a new state since child drawables contain local 992 // state but live within the parent's constant state. 993 // TODO: Move child drawables into local state. 994 ns = new RippleState(state, this, res); 995 } 996 997 if (res != null) { 998 mDensity = res.getDisplayMetrics().density; 999 } 1000 1001 mState = ns; 1002 mLayerState = ns; 1003 1004 if (ns.mNum > 0) { 1005 ensurePadding(); 1006 } 1007 1008 if (needsTheme) { 1009 applyTheme(theme); 1010 } 1011 1012 initializeFromState(); 1013 } 1014 1015 private void initializeFromState() { 1016 // Initialize from constant state. 1017 mMask = findDrawableByLayerId(R.id.mask); 1018 } 1019} 1020