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