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