1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.graphics.drawable; 18 19 import static java.lang.annotation.ElementType.FIELD; 20 import static java.lang.annotation.ElementType.LOCAL_VARIABLE; 21 import static java.lang.annotation.ElementType.METHOD; 22 import static java.lang.annotation.ElementType.PARAMETER; 23 import static java.lang.annotation.RetentionPolicy.SOURCE; 24 25 import android.animation.ValueAnimator; 26 import android.annotation.ColorInt; 27 import android.annotation.IntDef; 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.compat.annotation.UnsupportedAppUsage; 31 import android.content.pm.ActivityInfo.Config; 32 import android.content.res.ColorStateList; 33 import android.content.res.Resources; 34 import android.content.res.Resources.Theme; 35 import android.content.res.TypedArray; 36 import android.graphics.Bitmap; 37 import android.graphics.BitmapShader; 38 import android.graphics.Canvas; 39 import android.graphics.CanvasProperty; 40 import android.graphics.Color; 41 import android.graphics.ColorFilter; 42 import android.graphics.Matrix; 43 import android.graphics.Outline; 44 import android.graphics.Paint; 45 import android.graphics.PixelFormat; 46 import android.graphics.PorterDuff; 47 import android.graphics.PorterDuffColorFilter; 48 import android.graphics.RecordingCanvas; 49 import android.graphics.Rect; 50 import android.graphics.Shader; 51 import android.os.Build; 52 import android.os.Looper; 53 import android.util.AttributeSet; 54 import android.util.Log; 55 import android.view.animation.AnimationUtils; 56 import android.view.animation.LinearInterpolator; 57 58 import com.android.internal.R; 59 60 import org.xmlpull.v1.XmlPullParser; 61 import org.xmlpull.v1.XmlPullParserException; 62 63 import java.io.IOException; 64 import java.lang.annotation.Retention; 65 import java.lang.annotation.Target; 66 import java.util.ArrayList; 67 import java.util.Arrays; 68 69 /** 70 * Drawable that shows a ripple effect in response to state changes. The 71 * anchoring position of the ripple for a given state may be specified by 72 * calling {@link #setHotspot(float, float)} with the corresponding state 73 * attribute identifier. 74 * <p> 75 * A touch feedback drawable may contain multiple child layers, including a 76 * special mask layer that is not drawn to the screen. A single layer may be 77 * set as the mask from XML by specifying its {@code android:id} value as 78 * {@link android.R.id#mask}. At run time, a single layer may be set as the 79 * mask using {@code setId(..., android.R.id.mask)} or an existing mask layer 80 * may be replaced using {@code setDrawableByLayerId(android.R.id.mask, ...)}. 81 * <pre> 82 * <code><!-- A red ripple masked against an opaque rectangle. --/> 83 * <ripple android:color="#ffff0000"> 84 * <item android:id="@android:id/mask" 85 * android:drawable="@android:color/white" /> 86 * </ripple></code> 87 * </pre> 88 * <p> 89 * If a mask layer is set, the ripple effect will be masked against that layer 90 * before it is drawn over the composite of the remaining child layers. 91 * <p> 92 * If no mask layer is set, the ripple effect is masked against the composite 93 * of the child layers. 94 * <pre> 95 * <code><!-- A green ripple drawn atop a black rectangle. --/> 96 * <ripple android:color="#ff00ff00"> 97 * <item android:drawable="@android:color/black" /> 98 * </ripple> 99 * 100 * <!-- A blue ripple drawn atop a drawable resource. --/> 101 * <ripple android:color="#ff0000ff"> 102 * <item android:drawable="@drawable/my_drawable" /> 103 * </ripple></code> 104 * </pre> 105 * <p> 106 * If no child layers or mask is specified and the ripple is set as a View 107 * background, the ripple will be drawn atop the first available parent 108 * background within the View's hierarchy. In this case, the drawing region 109 * may extend outside of the Drawable bounds. 110 * <pre> 111 * <code><!-- An unbounded red ripple. --/> 112 * <ripple android:color="#ffff0000" /></code> 113 * </pre> 114 * 115 * @attr ref android.R.styleable#RippleDrawable_color 116 */ 117 public class RippleDrawable extends LayerDrawable { 118 private static final String TAG = "RippleDrawable"; 119 /** 120 * Radius value that specifies the ripple radius should be computed based 121 * on the size of the ripple's container. 122 */ 123 public static final int RADIUS_AUTO = -1; 124 125 /** 126 * Ripple style where a solid circle is drawn. This is also the default style 127 * @see #setRippleStyle(int) 128 * @hide 129 */ 130 public static final int STYLE_SOLID = 0; 131 /** 132 * Ripple style where a circle shape with a patterned, 133 * noisy interior expands from the hotspot to the bounds". 134 * @see #setRippleStyle(int) 135 * @hide 136 */ 137 public static final int STYLE_PATTERNED = 1; 138 139 /** 140 * Ripple drawing style 141 * @hide 142 */ 143 @Retention(SOURCE) 144 @Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) 145 @IntDef({STYLE_SOLID, STYLE_PATTERNED}) 146 public @interface RippleStyle { 147 } 148 149 private static final int BACKGROUND_OPACITY_DURATION = 80; 150 private static final int MASK_UNKNOWN = -1; 151 private static final int MASK_NONE = 0; 152 private static final int MASK_CONTENT = 1; 153 private static final int MASK_EXPLICIT = 2; 154 155 /** The maximum number of ripples supported. */ 156 private static final int MAX_RIPPLES = 10; 157 private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); 158 private static final int DEFAULT_EFFECT_COLOR = 0x8dffffff; 159 /** Temporary flag for teamfood. **/ 160 private static final boolean FORCE_PATTERNED_STYLE = true; 161 162 private final Rect mTempRect = new Rect(); 163 164 /** Current ripple effect bounds, used to constrain ripple effects. */ 165 private final Rect mHotspotBounds = new Rect(); 166 167 /** Current drawing bounds, used to compute dirty region. */ 168 private final Rect mDrawingBounds = new Rect(); 169 170 /** Current dirty bounds, union of current and previous drawing bounds. */ 171 private final Rect mDirtyBounds = new Rect(); 172 173 /** Mirrors mLayerState with some extra information. */ 174 @UnsupportedAppUsage(trackingBug = 175939224) 175 private RippleState mState; 176 177 /** The masking layer, e.g. the layer with id R.id.mask. */ 178 private Drawable mMask; 179 180 /** The current background. May be actively animating or pending entry. */ 181 private RippleBackground mBackground; 182 183 private Bitmap mMaskBuffer; 184 private BitmapShader mMaskShader; 185 private Canvas mMaskCanvas; 186 private Matrix mMaskMatrix; 187 private PorterDuffColorFilter mMaskColorFilter; 188 private PorterDuffColorFilter mFocusColorFilter; 189 private boolean mHasValidMask; 190 191 /** The current ripple. May be actively animating or pending entry. */ 192 private RippleForeground mRipple; 193 194 /** Whether we expect to draw a ripple when visible. */ 195 private boolean mRippleActive; 196 197 // Hotspot coordinates that are awaiting activation. 198 private float mPendingX; 199 private float mPendingY; 200 private boolean mHasPending; 201 202 /** 203 * Lazily-created array of actively animating ripples. Inactive ripples are 204 * pruned during draw(). The locations of these will not change. 205 */ 206 private RippleForeground[] mExitingRipples; 207 private int mExitingRipplesCount = 0; 208 209 /** Paint used to control appearance of ripples. */ 210 private Paint mRipplePaint; 211 212 /** Target density of the display into which ripples are drawn. */ 213 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 214 private int mDensity; 215 216 /** Whether bounds are being overridden. */ 217 private boolean mOverrideBounds; 218 219 /** 220 * If set, force all ripple animations to not run on RenderThread, even if it would be 221 * available. 222 */ 223 private boolean mForceSoftware; 224 225 // Patterned 226 private boolean mAddRipple = false; 227 private float mTargetBackgroundOpacity; 228 private ValueAnimator mBackgroundAnimation; 229 private float mBackgroundOpacity; 230 private boolean mRunBackgroundAnimation; 231 private boolean mExitingAnimation; 232 private ArrayList<RippleAnimationSession> mRunningAnimations = new ArrayList<>(); 233 234 /** 235 * Constructor used for drawable inflation. 236 */ RippleDrawable()237 RippleDrawable() { 238 this(new RippleState(null, null, null), null); 239 } 240 241 /** 242 * Creates a new ripple drawable with the specified ripple color and 243 * optional content and mask drawables. 244 * 245 * @param color The ripple color 246 * @param content The content drawable, may be {@code null} 247 * @param mask The mask drawable, may be {@code null} 248 */ RippleDrawable(@onNull ColorStateList color, @Nullable Drawable content, @Nullable Drawable mask)249 public RippleDrawable(@NonNull ColorStateList color, @Nullable Drawable content, 250 @Nullable Drawable mask) { 251 this(new RippleState(null, null, null), null); 252 253 if (color == null) { 254 throw new IllegalArgumentException("RippleDrawable requires a non-null color"); 255 } 256 257 if (content != null) { 258 addLayer(content, null, 0, 0, 0, 0, 0); 259 } 260 261 if (mask != null) { 262 addLayer(mask, null, android.R.id.mask, 0, 0, 0, 0); 263 } 264 265 setColor(color); 266 ensurePadding(); 267 refreshPadding(); 268 updateLocalState(); 269 } 270 271 @Override jumpToCurrentState()272 public void jumpToCurrentState() { 273 super.jumpToCurrentState(); 274 275 if (mRipple != null) { 276 mRipple.end(); 277 } 278 279 if (mBackground != null) { 280 mBackground.jumpToFinal(); 281 } 282 283 cancelExitingRipples(); 284 endPatternedAnimations(); 285 } 286 endPatternedAnimations()287 private void endPatternedAnimations() { 288 for (int i = 0; i < mRunningAnimations.size(); i++) { 289 RippleAnimationSession session = mRunningAnimations.get(i); 290 session.end(); 291 } 292 mRunningAnimations.clear(); 293 } 294 cancelExitingRipples()295 private void cancelExitingRipples() { 296 final int count = mExitingRipplesCount; 297 final RippleForeground[] ripples = mExitingRipples; 298 for (int i = 0; i < count; i++) { 299 ripples[i].end(); 300 } 301 302 if (ripples != null) { 303 Arrays.fill(ripples, 0, count, null); 304 } 305 mExitingRipplesCount = 0; 306 // Always draw an additional "clean" frame after canceling animations. 307 invalidateSelf(false); 308 } 309 310 @Override getOpacity()311 public int getOpacity() { 312 // Worst-case scenario. 313 return PixelFormat.TRANSLUCENT; 314 } 315 316 @Override onStateChange(int[] stateSet)317 protected boolean onStateChange(int[] stateSet) { 318 final boolean changed = super.onStateChange(stateSet); 319 320 boolean enabled = false; 321 boolean pressed = false; 322 boolean focused = false; 323 boolean hovered = false; 324 325 for (int state : stateSet) { 326 if (state == R.attr.state_enabled) { 327 enabled = true; 328 } else if (state == R.attr.state_focused) { 329 focused = true; 330 } else if (state == R.attr.state_pressed) { 331 pressed = true; 332 } else if (state == R.attr.state_hovered) { 333 hovered = true; 334 } 335 } 336 setRippleActive(enabled && pressed); 337 setBackgroundActive(hovered, focused, pressed); 338 339 return changed; 340 } 341 setRippleActive(boolean active)342 private void setRippleActive(boolean active) { 343 if (mRippleActive != active) { 344 mRippleActive = active; 345 if (mState.mRippleStyle == STYLE_SOLID) { 346 if (active) { 347 tryRippleEnter(); 348 } else { 349 tryRippleExit(); 350 } 351 } else { 352 if (active) { 353 startPatternedAnimation(); 354 } else { 355 exitPatternedAnimation(); 356 } 357 } 358 } 359 } 360 setBackgroundActive(boolean hovered, boolean focused, boolean pressed)361 private void setBackgroundActive(boolean hovered, boolean focused, boolean pressed) { 362 if (mState.mRippleStyle == STYLE_SOLID) { 363 if (mBackground == null && (hovered || focused)) { 364 mBackground = new RippleBackground(this, mHotspotBounds, isBounded()); 365 mBackground.setup(mState.mMaxRadius, mDensity); 366 } 367 if (mBackground != null) { 368 mBackground.setState(focused, hovered, pressed); 369 } 370 } else { 371 if (focused || hovered) { 372 if (!pressed) { 373 enterPatternedBackgroundAnimation(focused, hovered); 374 } 375 } else { 376 exitPatternedBackgroundAnimation(); 377 } 378 } 379 } 380 381 @Override onBoundsChange(Rect bounds)382 protected void onBoundsChange(Rect bounds) { 383 super.onBoundsChange(bounds); 384 385 if (!mOverrideBounds) { 386 mHotspotBounds.set(bounds); 387 onHotspotBoundsChanged(); 388 } 389 390 final int count = mExitingRipplesCount; 391 final RippleForeground[] ripples = mExitingRipples; 392 for (int i = 0; i < count; i++) { 393 ripples[i].onBoundsChange(); 394 } 395 396 if (mBackground != null) { 397 mBackground.onBoundsChange(); 398 } 399 400 if (mRipple != null) { 401 mRipple.onBoundsChange(); 402 } 403 invalidateSelf(); 404 } 405 406 @Override setVisible(boolean visible, boolean restart)407 public boolean setVisible(boolean visible, boolean restart) { 408 final boolean changed = super.setVisible(visible, restart); 409 410 if (!visible) { 411 clearHotspots(); 412 } else if (changed) { 413 // If we just became visible, ensure the background and ripple 414 // visibilities are consistent with their internal states. 415 if (mRippleActive) { 416 if (mState.mRippleStyle == STYLE_SOLID) { 417 tryRippleEnter(); 418 } else { 419 invalidateSelf(); 420 } 421 } 422 423 // Skip animations, just show the correct final states. 424 jumpToCurrentState(); 425 } 426 427 return changed; 428 } 429 430 /** 431 * @hide 432 */ 433 @Override isProjected()434 public boolean isProjected() { 435 // If the layer is bounded, then we don't need to project. 436 if (isBounded()) { 437 return false; 438 } 439 440 // Otherwise, if the maximum radius is contained entirely within the 441 // bounds then we don't need to project. This is sort of a hack to 442 // prevent check box ripples from being projected across the edges of 443 // scroll views. It does not impact rendering performance, and it can 444 // be removed once we have better handling of projection in scrollable 445 // views. 446 final int radius = mState.mMaxRadius; 447 final Rect drawableBounds = getBounds(); 448 final Rect hotspotBounds = mHotspotBounds; 449 if (radius != RADIUS_AUTO 450 && radius <= hotspotBounds.width() / 2 451 && radius <= hotspotBounds.height() / 2 452 && (drawableBounds.equals(hotspotBounds) 453 || drawableBounds.contains(hotspotBounds))) { 454 return false; 455 } 456 457 return true; 458 } 459 isBounded()460 private boolean isBounded() { 461 return getNumberOfLayers() > 0; 462 } 463 464 @Override isStateful()465 public boolean isStateful() { 466 return true; 467 } 468 469 @Override hasFocusStateSpecified()470 public boolean hasFocusStateSpecified() { 471 return true; 472 } 473 474 /** 475 * Sets the ripple color. 476 * 477 * @param color Ripple color as a color state list. 478 * 479 * @attr ref android.R.styleable#RippleDrawable_color 480 */ setColor(@onNull ColorStateList color)481 public void setColor(@NonNull ColorStateList color) { 482 if (color == null) { 483 throw new IllegalArgumentException("color cannot be null"); 484 } 485 mState.mColor = color; 486 invalidateSelf(false); 487 } 488 489 /** 490 * Sets the ripple effect color. 491 * 492 * @param color Ripple color as a color state list. 493 * 494 * @attr ref android.R.styleable#RippleDrawable_effectColor 495 */ setEffectColor(@onNull ColorStateList color)496 public void setEffectColor(@NonNull ColorStateList color) { 497 if (color == null) { 498 throw new IllegalArgumentException("color cannot be null"); 499 } 500 mState.mEffectColor = color; 501 invalidateSelf(false); 502 } 503 504 /** 505 * @return The ripple effect color as a color state list. 506 */ getEffectColor()507 public @NonNull ColorStateList getEffectColor() { 508 return mState.mEffectColor; 509 } 510 511 /** 512 * Sets the radius in pixels of the fully expanded ripple. 513 * 514 * @param radius ripple radius in pixels, or {@link #RADIUS_AUTO} to 515 * compute the radius based on the container size 516 * @attr ref android.R.styleable#RippleDrawable_radius 517 */ setRadius(int radius)518 public void setRadius(int radius) { 519 mState.mMaxRadius = radius; 520 invalidateSelf(false); 521 } 522 523 /** 524 * @return the radius in pixels of the fully expanded ripple if an explicit 525 * radius has been set, or {@link #RADIUS_AUTO} if the radius is 526 * computed based on the container size 527 * @attr ref android.R.styleable#RippleDrawable_radius 528 */ getRadius()529 public int getRadius() { 530 return mState.mMaxRadius; 531 } 532 533 @Override inflate(@onNull Resources r, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme)534 public void inflate(@NonNull Resources r, @NonNull XmlPullParser parser, 535 @NonNull AttributeSet attrs, @Nullable Theme theme) 536 throws XmlPullParserException, IOException { 537 final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.RippleDrawable); 538 539 // Force padding default to STACK before inflating. 540 setPaddingMode(PADDING_MODE_STACK); 541 542 // Inflation will advance the XmlPullParser and AttributeSet. 543 super.inflate(r, parser, attrs, theme); 544 545 updateStateFromTypedArray(a); 546 verifyRequiredAttributes(a); 547 a.recycle(); 548 549 updateLocalState(); 550 } 551 552 @Override setDrawableByLayerId(int id, Drawable drawable)553 public boolean setDrawableByLayerId(int id, Drawable drawable) { 554 if (super.setDrawableByLayerId(id, drawable)) { 555 if (id == R.id.mask) { 556 mMask = drawable; 557 mHasValidMask = false; 558 } 559 560 return true; 561 } 562 563 return false; 564 } 565 566 /** 567 * Specifies how layer padding should affect the bounds of subsequent 568 * layers. The default and recommended value for RippleDrawable is 569 * {@link #PADDING_MODE_STACK}. 570 * 571 * @param mode padding mode, one of: 572 * <ul> 573 * <li>{@link #PADDING_MODE_NEST} to nest each layer inside the 574 * padding of the previous layer 575 * <li>{@link #PADDING_MODE_STACK} to stack each layer directly 576 * atop the previous layer 577 * </ul> 578 * @see #getPaddingMode() 579 */ 580 @Override setPaddingMode(int mode)581 public void setPaddingMode(int mode) { 582 super.setPaddingMode(mode); 583 } 584 585 /** 586 * Initializes the constant state from the values in the typed array. 587 */ updateStateFromTypedArray(@onNull TypedArray a)588 private void updateStateFromTypedArray(@NonNull TypedArray a) throws XmlPullParserException { 589 final RippleState state = mState; 590 591 // Account for any configuration changes. 592 state.mChangingConfigurations |= a.getChangingConfigurations(); 593 594 // Extract the theme attributes, if any. 595 state.mTouchThemeAttrs = a.extractThemeAttrs(); 596 597 final ColorStateList color = a.getColorStateList(R.styleable.RippleDrawable_color); 598 if (color != null) { 599 mState.mColor = color; 600 } 601 602 final ColorStateList effectColor = 603 a.getColorStateList(R.styleable.RippleDrawable_effectColor); 604 if (effectColor != null) { 605 mState.mEffectColor = effectColor; 606 } 607 608 mState.mMaxRadius = a.getDimensionPixelSize( 609 R.styleable.RippleDrawable_radius, mState.mMaxRadius); 610 } 611 verifyRequiredAttributes(@onNull TypedArray a)612 private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException { 613 if (mState.mColor == null && (mState.mTouchThemeAttrs == null 614 || mState.mTouchThemeAttrs[R.styleable.RippleDrawable_color] == 0)) { 615 throw new XmlPullParserException(a.getPositionDescription() + 616 ": <ripple> requires a valid color attribute"); 617 } 618 } 619 620 @Override applyTheme(@onNull Theme t)621 public void applyTheme(@NonNull Theme t) { 622 super.applyTheme(t); 623 624 final RippleState state = mState; 625 if (state == null) { 626 return; 627 } 628 629 if (state.mTouchThemeAttrs != null) { 630 final TypedArray a = t.resolveAttributes(state.mTouchThemeAttrs, 631 R.styleable.RippleDrawable); 632 try { 633 updateStateFromTypedArray(a); 634 verifyRequiredAttributes(a); 635 } catch (XmlPullParserException e) { 636 rethrowAsRuntimeException(e); 637 } finally { 638 a.recycle(); 639 } 640 } 641 642 if (state.mColor != null && state.mColor.canApplyTheme()) { 643 state.mColor = state.mColor.obtainForTheme(t); 644 } 645 646 updateLocalState(); 647 } 648 649 @Override canApplyTheme()650 public boolean canApplyTheme() { 651 return (mState != null && mState.canApplyTheme()) || super.canApplyTheme(); 652 } 653 654 @Override setHotspot(float x, float y)655 public void setHotspot(float x, float y) { 656 mPendingX = x; 657 mPendingY = y; 658 if (mRipple == null || mBackground == null) { 659 mHasPending = true; 660 } 661 662 if (mRipple != null) { 663 mRipple.move(x, y); 664 } 665 } 666 667 /** 668 * Attempts to start an enter animation for the active hotspot. Fails if 669 * there are too many animating ripples. 670 */ tryRippleEnter()671 private void tryRippleEnter() { 672 if (mExitingRipplesCount >= MAX_RIPPLES) { 673 // This should never happen unless the user is tapping like a maniac 674 // or there is a bug that's preventing ripples from being removed. 675 return; 676 } 677 678 if (mRipple == null) { 679 final float x; 680 final float y; 681 if (mHasPending) { 682 mHasPending = false; 683 x = mPendingX; 684 y = mPendingY; 685 } else { 686 x = mHotspotBounds.exactCenterX(); 687 y = mHotspotBounds.exactCenterY(); 688 } 689 690 mRipple = new RippleForeground(this, mHotspotBounds, x, y, mForceSoftware); 691 } 692 693 mRipple.setup(mState.mMaxRadius, mDensity); 694 mRipple.enter(); 695 } 696 697 /** 698 * Attempts to start an exit animation for the active hotspot. Fails if 699 * there is no active hotspot. 700 */ tryRippleExit()701 private void tryRippleExit() { 702 if (mRipple != null) { 703 if (mExitingRipples == null) { 704 mExitingRipples = new RippleForeground[MAX_RIPPLES]; 705 } 706 mExitingRipples[mExitingRipplesCount++] = mRipple; 707 mRipple.exit(); 708 mRipple = null; 709 } 710 } 711 712 /** 713 * Cancels and removes the active ripple, all exiting ripples, and the 714 * background. Nothing will be drawn after this method is called. 715 */ clearHotspots()716 private void clearHotspots() { 717 if (mRipple != null) { 718 mRipple.end(); 719 mRipple = null; 720 mRippleActive = false; 721 } 722 723 if (mBackground != null) { 724 mBackground.setState(false, false, false); 725 } 726 727 cancelExitingRipples(); 728 endPatternedAnimations(); 729 } 730 731 @Override setHotspotBounds(int left, int top, int right, int bottom)732 public void setHotspotBounds(int left, int top, int right, int bottom) { 733 mOverrideBounds = true; 734 mHotspotBounds.set(left, top, right, bottom); 735 736 onHotspotBoundsChanged(); 737 } 738 739 @Override getHotspotBounds(Rect outRect)740 public void getHotspotBounds(Rect outRect) { 741 outRect.set(mHotspotBounds); 742 } 743 744 /** 745 * Notifies all the animating ripples that the hotspot bounds have changed and modify sessions. 746 */ onHotspotBoundsChanged()747 private void onHotspotBoundsChanged() { 748 final int count = mExitingRipplesCount; 749 final RippleForeground[] ripples = mExitingRipples; 750 for (int i = 0; i < count; i++) { 751 ripples[i].onHotspotBoundsChanged(); 752 } 753 754 if (mRipple != null) { 755 mRipple.onHotspotBoundsChanged(); 756 } 757 758 if (mBackground != null) { 759 mBackground.onHotspotBoundsChanged(); 760 } 761 float newRadius = Math.round(getComputedRadius()); 762 for (int i = 0; i < mRunningAnimations.size(); i++) { 763 RippleAnimationSession s = mRunningAnimations.get(i); 764 s.setRadius(newRadius); 765 s.getProperties().getShader() 766 .setResolution(mHotspotBounds.width(), mHotspotBounds.height()); 767 float cx = mHotspotBounds.centerX(), cy = mHotspotBounds.centerY(); 768 s.getProperties().getShader().setOrigin(cx, cy); 769 s.getProperties().setOrigin(cx, cy); 770 if (!s.isForceSoftware()) { 771 s.getCanvasProperties() 772 .setOrigin(CanvasProperty.createFloat(cx), CanvasProperty.createFloat(cy)); 773 } 774 } 775 } 776 777 /** 778 * Populates <code>outline</code> with the first available layer outline, 779 * excluding the mask layer. 780 * 781 * @param outline Outline in which to place the first available layer outline 782 */ 783 @Override getOutline(@onNull Outline outline)784 public void getOutline(@NonNull Outline outline) { 785 final LayerState state = mLayerState; 786 final ChildDrawable[] children = state.mChildren; 787 final int N = state.mNumChildren; 788 for (int i = 0; i < N; i++) { 789 if (children[i].mId != R.id.mask) { 790 children[i].mDrawable.getOutline(outline); 791 if (!outline.isEmpty()) return; 792 } 793 } 794 } 795 796 /** 797 * Optimized for drawing ripples with a mask layer and optional content. 798 */ 799 @Override draw(@onNull Canvas canvas)800 public void draw(@NonNull Canvas canvas) { 801 if (mState.mRippleStyle == STYLE_SOLID) { 802 drawSolid(canvas); 803 } else { 804 drawPatterned(canvas); 805 } 806 } 807 drawSolid(Canvas canvas)808 private void drawSolid(Canvas canvas) { 809 pruneRipples(); 810 811 // Clip to the dirty bounds, which will be the drawable bounds if we 812 // have a mask or content and the ripple bounds if we're projecting. 813 final Rect bounds = getDirtyBounds(); 814 final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 815 if (isBounded()) { 816 canvas.clipRect(bounds); 817 } 818 819 drawContent(canvas); 820 drawBackgroundAndRipples(canvas); 821 822 canvas.restoreToCount(saveCount); 823 } 824 exitPatternedBackgroundAnimation()825 private void exitPatternedBackgroundAnimation() { 826 mTargetBackgroundOpacity = 0; 827 if (mBackgroundAnimation != null) mBackgroundAnimation.cancel(); 828 // after cancel 829 mRunBackgroundAnimation = true; 830 invalidateSelf(false); 831 } 832 startPatternedAnimation()833 private void startPatternedAnimation() { 834 mAddRipple = true; 835 invalidateSelf(false); 836 } 837 exitPatternedAnimation()838 private void exitPatternedAnimation() { 839 mExitingAnimation = true; 840 invalidateSelf(false); 841 } 842 enterPatternedBackgroundAnimation(boolean focused, boolean hovered)843 private void enterPatternedBackgroundAnimation(boolean focused, boolean hovered) { 844 mBackgroundOpacity = 0; 845 mTargetBackgroundOpacity = focused ? .6f : hovered ? .2f : 0f; 846 if (mBackgroundAnimation != null) mBackgroundAnimation.cancel(); 847 // after cancel 848 mRunBackgroundAnimation = true; 849 invalidateSelf(false); 850 } 851 startBackgroundAnimation()852 private void startBackgroundAnimation() { 853 mRunBackgroundAnimation = false; 854 if (Looper.myLooper() == null) { 855 Log.w(TAG, "Thread doesn't have a looper. Skipping animation."); 856 return; 857 } 858 mBackgroundAnimation = ValueAnimator.ofFloat(mBackgroundOpacity, mTargetBackgroundOpacity); 859 mBackgroundAnimation.setInterpolator(LINEAR_INTERPOLATOR); 860 mBackgroundAnimation.setDuration(BACKGROUND_OPACITY_DURATION); 861 mBackgroundAnimation.addUpdateListener(update -> { 862 mBackgroundOpacity = (float) update.getAnimatedValue(); 863 invalidateSelf(false); 864 }); 865 mBackgroundAnimation.start(); 866 } 867 drawPatterned(@onNull Canvas canvas)868 private void drawPatterned(@NonNull Canvas canvas) { 869 final Rect bounds = mHotspotBounds; 870 final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); 871 boolean useCanvasProps = shouldUseCanvasProps(canvas); 872 if (isBounded()) { 873 canvas.clipRect(getDirtyBounds()); 874 } 875 final float x, y, cx, cy, w, h; 876 boolean addRipple = mAddRipple; 877 cx = bounds.centerX(); 878 cy = bounds.centerY(); 879 boolean shouldExit = mExitingAnimation; 880 mExitingAnimation = false; 881 mAddRipple = false; 882 if (mRunningAnimations.size() > 0 && !addRipple) { 883 // update paint when view is invalidated 884 getRipplePaint(); 885 } 886 drawContent(canvas); 887 drawPatternedBackground(canvas, cx, cy); 888 if (addRipple && mRunningAnimations.size() <= MAX_RIPPLES) { 889 if (mHasPending) { 890 x = mPendingX; 891 y = mPendingY; 892 mHasPending = false; 893 } else { 894 x = bounds.exactCenterX(); 895 y = bounds.exactCenterY(); 896 } 897 h = bounds.height(); 898 w = bounds.width(); 899 RippleAnimationSession.AnimationProperties<Float, Paint> properties = 900 createAnimationProperties(x, y, cx, cy, w, h); 901 mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps) 902 .setOnAnimationUpdated(() -> invalidateSelf(false)) 903 .setOnSessionEnd(session -> { 904 mRunningAnimations.remove(session); 905 }) 906 .setForceSoftwareAnimation(!useCanvasProps) 907 .enter(canvas)); 908 } 909 if (shouldExit) { 910 for (int i = 0; i < mRunningAnimations.size(); i++) { 911 RippleAnimationSession s = mRunningAnimations.get(i); 912 s.exit(canvas); 913 } 914 } 915 for (int i = 0; i < mRunningAnimations.size(); i++) { 916 RippleAnimationSession s = mRunningAnimations.get(i); 917 if (useCanvasProps) { 918 RippleAnimationSession.AnimationProperties<CanvasProperty<Float>, 919 CanvasProperty<Paint>> 920 p = s.getCanvasProperties(); 921 RecordingCanvas can = (RecordingCanvas) canvas; 922 can.drawRipple(p.getX(), p.getY(), p.getMaxRadius(), p.getPaint(), 923 p.getProgress(), p.getNoisePhase(), p.getColor(), p.getShader()); 924 } else { 925 RippleAnimationSession.AnimationProperties<Float, Paint> p = 926 s.getProperties(); 927 float radius = p.getMaxRadius(); 928 canvas.drawCircle(p.getX(), p.getY(), radius, p.getPaint()); 929 } 930 } 931 canvas.restoreToCount(saveCount); 932 } 933 drawPatternedBackground(Canvas c, float cx, float cy)934 private void drawPatternedBackground(Canvas c, float cx, float cy) { 935 if (mRunBackgroundAnimation) { 936 startBackgroundAnimation(); 937 } 938 if (mBackgroundOpacity == 0) return; 939 Paint p = getRipplePaint(); 940 float newOpacity = mBackgroundOpacity; 941 final int origAlpha = p.getAlpha(); 942 final int alpha = Math.min((int) (origAlpha * newOpacity + 0.5f), 255); 943 if (alpha > 0) { 944 ColorFilter origFilter = p.getColorFilter(); 945 p.setColorFilter(mFocusColorFilter); 946 p.setAlpha(alpha); 947 c.drawCircle(cx, cy, getComputedRadius(), p); 948 p.setAlpha(origAlpha); 949 p.setColorFilter(origFilter); 950 } 951 } 952 computeRadius()953 private float computeRadius() { 954 final float halfWidth = mHotspotBounds.width() / 2.0f; 955 final float halfHeight = mHotspotBounds.height() / 2.0f; 956 return (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight); 957 } 958 getComputedRadius()959 private int getComputedRadius() { 960 if (mState.mMaxRadius >= 0) return mState.mMaxRadius; 961 return (int) computeRadius(); 962 } 963 964 @NonNull createAnimationProperties( float x, float y, float cx, float cy, float w, float h)965 private RippleAnimationSession.AnimationProperties<Float, Paint> createAnimationProperties( 966 float x, float y, float cx, float cy, float w, float h) { 967 Paint p = new Paint(getRipplePaint()); 968 float radius = getComputedRadius(); 969 RippleAnimationSession.AnimationProperties<Float, Paint> properties; 970 RippleShader shader = new RippleShader(); 971 // Grab the color for the current state and cut the alpha channel in 972 // half so that the ripple and background together yield full alpha. 973 final int color = clampAlpha(mMaskColorFilter == null 974 ? mState.mColor.getColorForState(getState(), Color.BLACK) 975 : mMaskColorFilter.getColor()); 976 final int effectColor = mState.mEffectColor.getColorForState(getState(), Color.MAGENTA); 977 final float noisePhase = AnimationUtils.currentAnimationTimeMillis(); 978 shader.setColor(color, effectColor); 979 shader.setOrigin(cx, cy); 980 shader.setTouch(x, y); 981 shader.setResolution(w, h); 982 shader.setNoisePhase(noisePhase); 983 shader.setRadius(radius); 984 shader.setProgress(.0f); 985 properties = new RippleAnimationSession.AnimationProperties<>( 986 cx, cy, radius, noisePhase, p, 0f, color, shader); 987 if (mMaskShader == null) { 988 shader.setShader(null); 989 } else { 990 shader.setShader(mMaskShader); 991 } 992 p.setShader(shader); 993 p.setColorFilter(null); 994 p.setColor(color); 995 return properties; 996 } 997 clampAlpha(@olorInt int color)998 private int clampAlpha(@ColorInt int color) { 999 if (Color.alpha(color) > 128) { 1000 return (color & 0x00FFFFFF) | 0x80000000; 1001 } 1002 return color; 1003 } 1004 shouldUseCanvasProps(Canvas c)1005 private boolean shouldUseCanvasProps(Canvas c) { 1006 return !mForceSoftware && c.isHardwareAccelerated(); 1007 } 1008 1009 @Override invalidateSelf()1010 public void invalidateSelf() { 1011 invalidateSelf(true); 1012 } 1013 invalidateSelf(boolean invalidateMask)1014 void invalidateSelf(boolean invalidateMask) { 1015 super.invalidateSelf(); 1016 1017 if (invalidateMask) { 1018 // Force the mask to update on the next draw(). 1019 mHasValidMask = false; 1020 } 1021 1022 } 1023 pruneRipples()1024 private void pruneRipples() { 1025 int remaining = 0; 1026 1027 // Move remaining entries into pruned spaces. 1028 final RippleForeground[] ripples = mExitingRipples; 1029 final int count = mExitingRipplesCount; 1030 for (int i = 0; i < count; i++) { 1031 if (!ripples[i].hasFinishedExit()) { 1032 ripples[remaining++] = ripples[i]; 1033 } 1034 } 1035 1036 // Null out the remaining entries. 1037 for (int i = remaining; i < count; i++) { 1038 ripples[i] = null; 1039 } 1040 1041 mExitingRipplesCount = remaining; 1042 } 1043 1044 /** 1045 * @return whether we need to use a mask 1046 */ updateMaskShaderIfNeeded()1047 private void updateMaskShaderIfNeeded() { 1048 if (mHasValidMask) { 1049 return; 1050 } 1051 1052 final int maskType = getMaskType(); 1053 if (maskType == MASK_UNKNOWN) { 1054 return; 1055 } 1056 1057 mHasValidMask = true; 1058 1059 final Rect bounds = getBounds(); 1060 if (maskType == MASK_NONE || bounds.isEmpty()) { 1061 if (mMaskBuffer != null) { 1062 mMaskBuffer.recycle(); 1063 mMaskBuffer = null; 1064 mMaskShader = null; 1065 mMaskCanvas = null; 1066 } 1067 mMaskMatrix = null; 1068 mMaskColorFilter = null; 1069 return; 1070 } 1071 1072 // Ensure we have a correctly-sized buffer. 1073 if (mMaskBuffer == null 1074 || mMaskBuffer.getWidth() != bounds.width() 1075 || mMaskBuffer.getHeight() != bounds.height()) { 1076 if (mMaskBuffer != null) { 1077 mMaskBuffer.recycle(); 1078 } 1079 1080 mMaskBuffer = Bitmap.createBitmap( 1081 bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); 1082 mMaskShader = new BitmapShader(mMaskBuffer, 1083 Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 1084 mMaskCanvas = new Canvas(mMaskBuffer); 1085 } else { 1086 mMaskBuffer.eraseColor(Color.TRANSPARENT); 1087 } 1088 1089 if (mMaskMatrix == null) { 1090 mMaskMatrix = new Matrix(); 1091 } else { 1092 mMaskMatrix.reset(); 1093 } 1094 1095 if (mMaskColorFilter == null) { 1096 mMaskColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN); 1097 mFocusColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_IN); 1098 } 1099 1100 // Draw the appropriate mask anchored to (0,0). 1101 final int left = bounds.left; 1102 final int top = bounds.top; 1103 if (mState.mRippleStyle == STYLE_SOLID) { 1104 mMaskCanvas.translate(-left, -top); 1105 } 1106 if (maskType == MASK_EXPLICIT) { 1107 drawMask(mMaskCanvas); 1108 } else if (maskType == MASK_CONTENT) { 1109 drawContent(mMaskCanvas); 1110 } 1111 if (mState.mRippleStyle == STYLE_SOLID) { 1112 mMaskCanvas.translate(left, top); 1113 } 1114 if (mState.mRippleStyle == STYLE_PATTERNED) { 1115 for (int i = 0; i < mRunningAnimations.size(); i++) { 1116 mRunningAnimations.get(i).getProperties().getShader().setShader(mMaskShader); 1117 } 1118 } 1119 } 1120 getMaskType()1121 private int getMaskType() { 1122 if (mRipple == null && mExitingRipplesCount <= 0 1123 && (mBackground == null || !mBackground.isVisible()) 1124 && mState.mRippleStyle == STYLE_SOLID) { 1125 // We might need a mask later. 1126 return MASK_UNKNOWN; 1127 } 1128 1129 if (mMask != null) { 1130 if (mMask.getOpacity() == PixelFormat.OPAQUE) { 1131 // Clipping handles opaque explicit masks. 1132 return MASK_NONE; 1133 } else { 1134 return MASK_EXPLICIT; 1135 } 1136 } 1137 1138 // Check for non-opaque, non-mask content. 1139 final ChildDrawable[] array = mLayerState.mChildren; 1140 final int count = mLayerState.mNumChildren; 1141 for (int i = 0; i < count; i++) { 1142 if (array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) { 1143 return MASK_CONTENT; 1144 } 1145 } 1146 1147 // Clipping handles opaque content. 1148 return MASK_NONE; 1149 } 1150 drawContent(Canvas canvas)1151 private void drawContent(Canvas canvas) { 1152 // Draw everything except the mask. 1153 final ChildDrawable[] array = mLayerState.mChildren; 1154 final int count = mLayerState.mNumChildren; 1155 for (int i = 0; i < count; i++) { 1156 if (array[i].mId != R.id.mask) { 1157 array[i].mDrawable.draw(canvas); 1158 } 1159 } 1160 } 1161 drawBackgroundAndRipples(Canvas canvas)1162 private void drawBackgroundAndRipples(Canvas canvas) { 1163 final RippleForeground active = mRipple; 1164 final RippleBackground background = mBackground; 1165 final int count = mExitingRipplesCount; 1166 if (active == null && count <= 0 && (background == null || !background.isVisible())) { 1167 // Move along, nothing to draw here. 1168 return; 1169 } 1170 1171 final float x = mHotspotBounds.exactCenterX(); 1172 final float y = mHotspotBounds.exactCenterY(); 1173 canvas.translate(x, y); 1174 1175 final Paint p = getRipplePaint(); 1176 1177 if (background != null && background.isVisible()) { 1178 background.draw(canvas, p); 1179 } 1180 1181 if (count > 0) { 1182 final RippleForeground[] ripples = mExitingRipples; 1183 for (int i = 0; i < count; i++) { 1184 ripples[i].draw(canvas, p); 1185 } 1186 } 1187 1188 if (active != null) { 1189 active.draw(canvas, p); 1190 } 1191 1192 canvas.translate(-x, -y); 1193 } 1194 drawMask(Canvas canvas)1195 private void drawMask(Canvas canvas) { 1196 mMask.draw(canvas); 1197 } 1198 1199 @UnsupportedAppUsage getRipplePaint()1200 Paint getRipplePaint() { 1201 if (mRipplePaint == null) { 1202 mRipplePaint = new Paint(); 1203 mRipplePaint.setAntiAlias(true); 1204 mRipplePaint.setStyle(Paint.Style.FILL); 1205 } 1206 1207 final float x = mHotspotBounds.exactCenterX(); 1208 final float y = mHotspotBounds.exactCenterY(); 1209 1210 updateMaskShaderIfNeeded(); 1211 1212 // Position the shader to account for canvas translation. 1213 if (mMaskShader != null && mState.mRippleStyle == STYLE_SOLID) { 1214 final Rect bounds = getBounds(); 1215 mMaskMatrix.setTranslate(bounds.left - x, bounds.top - y); 1216 mMaskShader.setLocalMatrix(mMaskMatrix); 1217 } 1218 1219 // Grab the color for the current state and cut the alpha channel in 1220 // half so that the ripple and background together yield full alpha. 1221 final int color = clampAlpha(mState.mColor.getColorForState(getState(), Color.BLACK)); 1222 final Paint p = mRipplePaint; 1223 1224 if (mMaskColorFilter != null) { 1225 // The ripple timing depends on the paint's alpha value, so we need 1226 // to push just the alpha channel into the paint and let the filter 1227 // handle the full-alpha color. 1228 int maskColor = mState.mRippleStyle == STYLE_PATTERNED ? color : color | 0xFF000000; 1229 if (mMaskColorFilter.getColor() != maskColor) { 1230 mMaskColorFilter = new PorterDuffColorFilter(maskColor, mMaskColorFilter.getMode()); 1231 mFocusColorFilter = new PorterDuffColorFilter(color | 0xFF000000, 1232 mFocusColorFilter.getMode()); 1233 } 1234 p.setColor(color & 0xFF000000); 1235 p.setColorFilter(mMaskColorFilter); 1236 p.setShader(mMaskShader); 1237 } else { 1238 p.setColor(color); 1239 p.setColorFilter(null); 1240 p.setShader(null); 1241 } 1242 1243 return p; 1244 } 1245 1246 @Override getDirtyBounds()1247 public Rect getDirtyBounds() { 1248 if (!isBounded()) { 1249 final Rect drawingBounds = mDrawingBounds; 1250 final Rect dirtyBounds = mDirtyBounds; 1251 dirtyBounds.set(drawingBounds); 1252 drawingBounds.setEmpty(); 1253 1254 final int cX = (int) mHotspotBounds.exactCenterX(); 1255 final int cY = (int) mHotspotBounds.exactCenterY(); 1256 final Rect rippleBounds = mTempRect; 1257 1258 final RippleForeground[] activeRipples = mExitingRipples; 1259 final int N = mExitingRipplesCount; 1260 for (int i = 0; i < N; i++) { 1261 activeRipples[i].getBounds(rippleBounds); 1262 rippleBounds.offset(cX, cY); 1263 drawingBounds.union(rippleBounds); 1264 } 1265 1266 final RippleBackground background = mBackground; 1267 if (background != null) { 1268 background.getBounds(rippleBounds); 1269 rippleBounds.offset(cX, cY); 1270 drawingBounds.union(rippleBounds); 1271 } 1272 1273 dirtyBounds.union(drawingBounds); 1274 dirtyBounds.union(super.getDirtyBounds()); 1275 return dirtyBounds; 1276 } else { 1277 return getBounds(); 1278 } 1279 } 1280 1281 /** 1282 * Sets whether to disable RenderThread animations for this ripple. 1283 * 1284 * @param forceSoftware true if RenderThread animations should be disabled, false otherwise 1285 * @hide 1286 */ 1287 @UnsupportedAppUsage setForceSoftware(boolean forceSoftware)1288 public void setForceSoftware(boolean forceSoftware) { 1289 mForceSoftware = forceSoftware; 1290 } 1291 1292 @Override getConstantState()1293 public ConstantState getConstantState() { 1294 return mState; 1295 } 1296 1297 @Override mutate()1298 public Drawable mutate() { 1299 super.mutate(); 1300 1301 // LayerDrawable creates a new state using createConstantState, so 1302 // this should always be a safe cast. 1303 mState = (RippleState) mLayerState; 1304 1305 // The locally cached drawable may have changed. 1306 mMask = findDrawableByLayerId(R.id.mask); 1307 1308 return this; 1309 } 1310 1311 @Override createConstantState(LayerState state, Resources res)1312 RippleState createConstantState(LayerState state, Resources res) { 1313 return new RippleState(state, this, res); 1314 } 1315 1316 static class RippleState extends LayerState { 1317 int[] mTouchThemeAttrs; 1318 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 1319 ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); 1320 ColorStateList mEffectColor = ColorStateList.valueOf(DEFAULT_EFFECT_COLOR); 1321 int mMaxRadius = RADIUS_AUTO; 1322 int mRippleStyle = FORCE_PATTERNED_STYLE ? STYLE_PATTERNED : STYLE_SOLID; 1323 RippleState(LayerState orig, RippleDrawable owner, Resources res)1324 public RippleState(LayerState orig, RippleDrawable owner, Resources res) { 1325 super(orig, owner, res); 1326 1327 if (orig != null && orig instanceof RippleState) { 1328 final RippleState origs = (RippleState) orig; 1329 mTouchThemeAttrs = origs.mTouchThemeAttrs; 1330 mColor = origs.mColor; 1331 mMaxRadius = origs.mMaxRadius; 1332 mRippleStyle = origs.mRippleStyle; 1333 mEffectColor = origs.mEffectColor; 1334 1335 if (origs.mDensity != mDensity) { 1336 applyDensityScaling(orig.mDensity, mDensity); 1337 } 1338 } 1339 } 1340 1341 @Override onDensityChanged(int sourceDensity, int targetDensity)1342 protected void onDensityChanged(int sourceDensity, int targetDensity) { 1343 super.onDensityChanged(sourceDensity, targetDensity); 1344 1345 applyDensityScaling(sourceDensity, targetDensity); 1346 } 1347 applyDensityScaling(int sourceDensity, int targetDensity)1348 private void applyDensityScaling(int sourceDensity, int targetDensity) { 1349 if (mMaxRadius != RADIUS_AUTO) { 1350 mMaxRadius = Drawable.scaleFromDensity( 1351 mMaxRadius, sourceDensity, targetDensity, true); 1352 } 1353 } 1354 1355 @Override canApplyTheme()1356 public boolean canApplyTheme() { 1357 return mTouchThemeAttrs != null 1358 || (mColor != null && mColor.canApplyTheme()) 1359 || super.canApplyTheme(); 1360 } 1361 1362 @Override newDrawable()1363 public Drawable newDrawable() { 1364 return new RippleDrawable(this, null); 1365 } 1366 1367 @Override newDrawable(Resources res)1368 public Drawable newDrawable(Resources res) { 1369 return new RippleDrawable(this, res); 1370 } 1371 1372 @Override getChangingConfigurations()1373 public @Config int getChangingConfigurations() { 1374 return super.getChangingConfigurations() 1375 | (mColor != null ? mColor.getChangingConfigurations() : 0); 1376 } 1377 } 1378 RippleDrawable(RippleState state, Resources res)1379 private RippleDrawable(RippleState state, Resources res) { 1380 mState = new RippleState(state, this, res); 1381 mLayerState = mState; 1382 mDensity = Drawable.resolveDensity(res, mState.mDensity); 1383 1384 if (mState.mNumChildren > 0) { 1385 ensurePadding(); 1386 refreshPadding(); 1387 } 1388 1389 updateLocalState(); 1390 } 1391 updateLocalState()1392 private void updateLocalState() { 1393 // Initialize from constant state. 1394 mMask = findDrawableByLayerId(R.id.mask); 1395 } 1396 } 1397