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