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 com.android.systemui.statusbar.notification.row; 18 19 import static com.android.systemui.Flags.notificationBackgroundTintOptimization; 20 import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.BOTTOM; 21 import static com.android.systemui.statusbar.notification.row.ExpandableView.ClipSide.TOP; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.animation.ValueAnimator; 26 import android.content.Context; 27 import android.graphics.Canvas; 28 import android.graphics.Point; 29 import android.util.AttributeSet; 30 import android.util.IndentingPrintWriter; 31 import android.util.MathUtils; 32 import android.view.Choreographer; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.animation.Interpolator; 36 37 import com.android.app.animation.Interpolators; 38 import com.android.internal.jank.InteractionJankMonitor; 39 import com.android.internal.jank.InteractionJankMonitor.Configuration; 40 import com.android.settingslib.Utils; 41 import com.android.systemui.Gefingerpoken; 42 import com.android.systemui.res.R; 43 import com.android.systemui.shade.TouchLogger; 44 import com.android.systemui.statusbar.NotificationShelf; 45 import com.android.systemui.statusbar.notification.FakeShadowView; 46 import com.android.systemui.statusbar.notification.NotificationUtils; 47 import com.android.systemui.statusbar.notification.SourceType; 48 import com.android.systemui.statusbar.notification.shared.NotificationHeadsUpCycling; 49 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor; 50 import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; 51 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; 52 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 53 import com.android.systemui.util.DumpUtilsKt; 54 55 import java.io.PrintWriter; 56 import java.util.HashSet; 57 import java.util.Set; 58 59 /** 60 * Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf} 61 * to implement dimming/activating on Keyguard for the double-tap gesture 62 */ 63 public abstract class ActivatableNotificationView extends ExpandableOutlineView { 64 65 /** 66 * A sentinel value when no color should be used. Can be used with {@link #setTintColor(int)} 67 * or {@link #setOverrideTintColor(int, float)}. 68 */ 69 protected static final int NO_COLOR = 0; 70 /** 71 * The content of the view should start showing at animation progress value of 72 * #ALPHA_APPEAR_START_FRACTION. 73 */ 74 75 private static final float ALPHA_APPEAR_START_FRACTION = .7f; 76 /** 77 * The content should show fully with progress at #ALPHA_APPEAR_END_FRACTION 78 * The start of the animation is at #ALPHA_APPEAR_START_FRACTION 79 */ 80 private static final float ALPHA_APPEAR_END_FRACTION = 1; 81 private final Set<SourceType> mOnDetachResetRoundness = new HashSet<>(); 82 private int mTintedRippleColor; 83 private int mNormalRippleColor; 84 private Gefingerpoken mTouchHandler; 85 86 int mBgTint = NO_COLOR; 87 88 /** 89 * Flag to indicate that the notification has been touched once and the second touch will 90 * click it. 91 */ 92 private boolean mActivated; 93 94 private Interpolator mCurrentAppearInterpolator; 95 protected NotificationBackgroundView mBackgroundNormal; 96 private float mAnimationTranslationY; 97 private boolean mDrawingAppearAnimation; 98 private ValueAnimator mAppearAnimator; 99 private ValueAnimator mBackgroundColorAnimator; 100 private float mAppearAnimationFraction = -1.0f; 101 private float mAppearAnimationTranslation; 102 private int mNormalColor; 103 private boolean mIsBelowSpeedBump; 104 private long mLastActionUpTime; 105 106 private float mNormalBackgroundVisibilityAmount; 107 private FakeShadowView mFakeShadow; 108 private int mCurrentBackgroundTint; 109 private int mTargetTint; 110 private int mStartTint; 111 private int mOverrideTint; 112 private float mOverrideAmount; 113 private boolean mShadowHidden; 114 private boolean mIsHeadsUpAnimation; 115 /* In order to track headsup longpress coorindate. */ 116 protected Point mTargetPoint; 117 private boolean mDismissed; 118 private boolean mRefocusOnDismiss; 119 ActivatableNotificationView(Context context, AttributeSet attrs)120 public ActivatableNotificationView(Context context, AttributeSet attrs) { 121 super(context, attrs); 122 setClipChildren(false); 123 setClipToPadding(false); 124 updateColors(); 125 } 126 updateColors()127 private void updateColors() { 128 mNormalColor = Utils.getColorAttrDefaultColor(mContext, 129 com.android.internal.R.attr.materialColorSurfaceContainerHigh); 130 mTintedRippleColor = mContext.getColor( 131 R.color.notification_ripple_tinted_color); 132 mNormalRippleColor = mContext.getColor( 133 R.color.notification_ripple_untinted_color); 134 // Reset background color tint and override tint, as they are from an old theme 135 mBgTint = NO_COLOR; 136 mOverrideTint = NO_COLOR; 137 mOverrideAmount = 0.0f; 138 } 139 140 /** 141 * Reload background colors from resources and invalidate views. 142 */ updateBackgroundColors()143 public void updateBackgroundColors() { 144 updateColors(); 145 initBackground(); 146 updateBackgroundTint(); 147 } 148 getNormalBgColor()149 protected int getNormalBgColor() { 150 return mNormalColor; 151 } 152 153 /** 154 * @param width The actual width to apply to the background view. 155 */ setBackgroundWidth(int width)156 public void setBackgroundWidth(int width) { 157 if (mBackgroundNormal == null) { 158 return; 159 } 160 mBackgroundNormal.setActualWidth(width); 161 } 162 163 @Override onFinishInflate()164 protected void onFinishInflate() { 165 super.onFinishInflate(); 166 mBackgroundNormal = findViewById(R.id.backgroundNormal); 167 mFakeShadow = findViewById(R.id.fake_shadow); 168 mShadowHidden = mFakeShadow.getVisibility() != VISIBLE; 169 initBackground(); 170 updateBackgroundTint(); 171 updateOutlineAlpha(); 172 } 173 174 /** 175 * Sets the custom background on {@link #mBackgroundNormal} 176 * This method can also be used to reload the backgrounds on both of those views, which can 177 * be useful in a configuration change. 178 */ initBackground()179 protected void initBackground() { 180 mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg); 181 } 182 hideBackground()183 protected boolean hideBackground() { 184 return false; 185 } 186 updateBackground()187 protected void updateBackground() { 188 mBackgroundNormal.setVisibility(hideBackground() ? INVISIBLE : VISIBLE); 189 } 190 191 192 @Override onInterceptTouchEvent(MotionEvent ev)193 public boolean onInterceptTouchEvent(MotionEvent ev) { 194 if (mTouchHandler != null && mTouchHandler.onInterceptTouchEvent(ev)) { 195 return true; 196 } 197 return super.onInterceptTouchEvent(ev); 198 } 199 200 /** Sets the last action up time this view was touched. */ setLastActionUpTime(long eventTime)201 public void setLastActionUpTime(long eventTime) { 202 mLastActionUpTime = eventTime; 203 } 204 205 /** 206 * Returns the last action up time. The last time will also be cleared because the source of 207 * action is not only from touch event. That prevents the caller from utilizing the time with 208 * unrelated event. The time can be 0 if the event is unavailable. 209 */ getAndResetLastActionUpTime()210 public long getAndResetLastActionUpTime() { 211 long lastActionUpTime = mLastActionUpTime; 212 mLastActionUpTime = 0; 213 return lastActionUpTime; 214 } 215 disallowSingleClick(MotionEvent ev)216 protected boolean disallowSingleClick(MotionEvent ev) { 217 return false; 218 } 219 220 /** 221 * @return whether this view is interactive and can be double tapped 222 */ isInteractive()223 protected boolean isInteractive() { 224 return true; 225 } 226 227 @Override drawableStateChanged()228 protected void drawableStateChanged() { 229 super.drawableStateChanged(); 230 mBackgroundNormal.setState(getDrawableState()); 231 } 232 updateOutlineAlpha()233 private void updateOutlineAlpha() { 234 float alpha = NotificationStackScrollLayout.BACKGROUND_ALPHA_DIMMED; 235 alpha = (alpha + (1.0f - alpha) * mNormalBackgroundVisibilityAmount); 236 setOutlineAlpha(alpha); 237 } 238 239 @Override setBelowSpeedBump(boolean below)240 public void setBelowSpeedBump(boolean below) { 241 NotificationIconContainerRefactor.assertInLegacyMode(); 242 super.setBelowSpeedBump(below); 243 if (below != mIsBelowSpeedBump) { 244 mIsBelowSpeedBump = below; 245 updateBackgroundTint(); 246 } 247 } 248 249 /** 250 * Sets the tint color of the background 251 */ setTintColor(int color)252 protected void setTintColor(int color) { 253 setTintColor(color, false); 254 } 255 256 /** 257 * Sets the tint color of the background 258 */ setTintColor(int color, boolean animated)259 void setTintColor(int color, boolean animated) { 260 if (color != mBgTint) { 261 mBgTint = color; 262 updateBackgroundTint(animated); 263 } 264 } 265 266 /** 267 * Set an override tint color that is used for the background. 268 * 269 * @param color the color that should be used to tint the background. 270 * This can be {@link #NO_COLOR} if the tint should be normally computed. 271 * @param overrideAmount a value from 0 to 1 how much the override tint should be used. The 272 * background color will then be the interpolation between this and the 273 * regular background color, where 1 means the overrideTintColor is fully 274 * used and the background color not at all. 275 */ setOverrideTintColor(int color, float overrideAmount)276 public void setOverrideTintColor(int color, float overrideAmount) { 277 mOverrideTint = color; 278 mOverrideAmount = overrideAmount; 279 int newColor = calculateBgColor(); 280 setBackgroundTintColor(newColor); 281 } 282 updateBackgroundTint()283 protected void updateBackgroundTint() { 284 updateBackgroundTint(false /* animated */); 285 } 286 updateBackgroundTint(boolean animated)287 private void updateBackgroundTint(boolean animated) { 288 if (mBackgroundColorAnimator != null) { 289 mBackgroundColorAnimator.cancel(); 290 } 291 int rippleColor = getRippleColor(); 292 mBackgroundNormal.setRippleColor(rippleColor); 293 int color = calculateBgColor(); 294 if (!animated) { 295 setBackgroundTintColor(color); 296 } else if (color != mCurrentBackgroundTint) { 297 mStartTint = mCurrentBackgroundTint; 298 mTargetTint = color; 299 mBackgroundColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); 300 mBackgroundColorAnimator.addUpdateListener(animation -> { 301 int newColor = NotificationUtils.interpolateColors(mStartTint, mTargetTint, 302 animation.getAnimatedFraction()); 303 setBackgroundTintColor(newColor); 304 }); 305 mBackgroundColorAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 306 mBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR); 307 mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() { 308 @Override 309 public void onAnimationEnd(Animator animation) { 310 mBackgroundColorAnimator = null; 311 } 312 }); 313 mBackgroundColorAnimator.start(); 314 } 315 } 316 setBackgroundTintColor(int color)317 protected void setBackgroundTintColor(int color) { 318 if (color != mCurrentBackgroundTint) { 319 mCurrentBackgroundTint = color; 320 if (notificationBackgroundTintOptimization() && color == mNormalColor) { 321 // We don't need to tint a normal notification 322 color = 0; 323 } 324 mBackgroundNormal.setTint(color); 325 } 326 } 327 updateBackgroundClipping()328 protected void updateBackgroundClipping() { 329 mBackgroundNormal.setBottomAmountClips(!isChildInGroup()); 330 } 331 332 @Override onLayout(boolean changed, int left, int top, int right, int bottom)333 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 334 super.onLayout(changed, left, top, right, bottom); 335 setPivotX(getWidth() / 2); 336 } 337 338 @Override setActualHeight(int actualHeight, boolean notifyListeners)339 public void setActualHeight(int actualHeight, boolean notifyListeners) { 340 super.setActualHeight(actualHeight, notifyListeners); 341 setPivotY(actualHeight / 2); 342 mBackgroundNormal.setActualHeight(actualHeight); 343 } 344 345 @Override setClipTopAmount(int clipTopAmount)346 public void setClipTopAmount(int clipTopAmount) { 347 super.setClipTopAmount(clipTopAmount); 348 mBackgroundNormal.setClipTopAmount(clipTopAmount); 349 } 350 351 @Override setClipBottomAmount(int clipBottomAmount)352 public void setClipBottomAmount(int clipBottomAmount) { 353 super.setClipBottomAmount(clipBottomAmount); 354 mBackgroundNormal.setClipBottomAmount(clipBottomAmount); 355 } 356 357 @Override performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, Runnable onStartedRunnable, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener, ClipSide clipSide)358 public long performRemoveAnimation(long duration, long delay, float translationDirection, 359 boolean isHeadsUpAnimation, Runnable onStartedRunnable, Runnable onFinishedRunnable, 360 AnimatorListenerAdapter animationListener, ClipSide clipSide) { 361 enableAppearDrawing(true); 362 mIsHeadsUpAnimation = isHeadsUpAnimation; 363 if (mDrawingAppearAnimation) { 364 startAppearAnimation(false /* isAppearing */, translationDirection, 365 delay, duration, onStartedRunnable, onFinishedRunnable, animationListener, 366 clipSide); 367 } else { 368 if (onStartedRunnable != null) { 369 onStartedRunnable.run(); 370 } 371 if (onFinishedRunnable != null) { 372 onFinishedRunnable.run(); 373 } 374 } 375 return 0; 376 } 377 378 @Override performAddAnimation(long delay, long duration, boolean isHeadsUpAppear, Runnable onFinishRunnable)379 public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear, 380 Runnable onFinishRunnable) { 381 enableAppearDrawing(true); 382 mIsHeadsUpAnimation = isHeadsUpAppear; 383 if (mDrawingAppearAnimation) { 384 startAppearAnimation(true /* isAppearing */, isHeadsUpAppear ? 0.0f : -1.0f, delay, 385 duration, null, null, null, ClipSide.BOTTOM); 386 } 387 } 388 startAppearAnimation(boolean isAppearing, float translationDirection, long delay, long duration, final Runnable onStartedRunnable, final Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener, ClipSide clipSide)389 private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay, 390 long duration, final Runnable onStartedRunnable, final Runnable onFinishedRunnable, 391 AnimatorListenerAdapter animationListener, ClipSide clipSide) { 392 mAnimationTranslationY = translationDirection * getActualHeight(); 393 cancelAppearAnimation(); 394 if (mAppearAnimationFraction == -1.0f) { 395 // not initialized yet, we start anew 396 if (isAppearing) { 397 mAppearAnimationFraction = 0.0f; 398 mAppearAnimationTranslation = mAnimationTranslationY; 399 } else { 400 mAppearAnimationFraction = 1.0f; 401 mAppearAnimationTranslation = 0; 402 } 403 } 404 405 float targetValue; 406 if (isAppearing) { 407 mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN; 408 targetValue = 1.0f; 409 } else { 410 mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE; 411 targetValue = 0.0f; 412 } 413 414 if (NotificationHeadsUpCycling.isEnabled()) { 415 // TODO(b/316404716): add avalanche filtering 416 mCurrentAppearInterpolator = Interpolators.LINEAR; 417 } 418 419 mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction, 420 targetValue); 421 if (NotificationsImprovedHunAnimation.isEnabled() 422 || NotificationHeadsUpCycling.isEnabled()) { 423 mAppearAnimator.setInterpolator(mCurrentAppearInterpolator); 424 } else { 425 mAppearAnimator.setInterpolator(Interpolators.LINEAR); 426 } 427 mAppearAnimator.setDuration( 428 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue))); 429 mAppearAnimator.addUpdateListener(animation -> { 430 mAppearAnimationFraction = (float) animation.getAnimatedValue(); 431 updateAppearAnimationAlpha(); 432 if (NotificationHeadsUpCycling.isEnabled()) { 433 // For cycling out, we want the HUN to be clipped from the top. 434 updateAppearRect(clipSide); 435 } else { 436 updateAppearRect(); 437 } 438 invalidate(); 439 }); 440 if (animationListener != null) { 441 mAppearAnimator.addListener(animationListener); 442 } 443 // we need to apply the initial state already to avoid drawn frames in the wrong state 444 updateAppearAnimationAlpha(); 445 if (NotificationHeadsUpCycling.isEnabled()) { 446 updateAppearRect(clipSide); 447 } else { 448 updateAppearRect(); 449 } 450 mAppearAnimator.addListener(new AnimatorListenerAdapter() { 451 private boolean mRunWithoutInterruptions; 452 453 @Override 454 public void onAnimationEnd(Animator animation) { 455 if (onFinishedRunnable != null) { 456 onFinishedRunnable.run(); 457 } 458 if (mRunWithoutInterruptions) { 459 enableAppearDrawing(false); 460 } 461 462 // We need to reset the View state, even if the animation was cancelled 463 onAppearAnimationFinished(isAppearing); 464 465 if (mRunWithoutInterruptions) { 466 InteractionJankMonitor.getInstance().end(getCujType(isAppearing)); 467 } else { 468 InteractionJankMonitor.getInstance().cancel(getCujType(isAppearing)); 469 } 470 } 471 472 @Override 473 public void onAnimationStart(Animator animation) { 474 if (onStartedRunnable != null) { 475 onStartedRunnable.run(); 476 } 477 mRunWithoutInterruptions = true; 478 Configuration.Builder builder = Configuration.Builder 479 .withView(getCujType(isAppearing), ActivatableNotificationView.this); 480 InteractionJankMonitor.getInstance().begin(builder); 481 } 482 483 @Override 484 public void onAnimationCancel(Animator animation) { 485 mRunWithoutInterruptions = false; 486 } 487 }); 488 489 // Cache the original animator so we can check if the animation should be started in the 490 // Choreographer callback. It's possible that the original animator (mAppearAnimator) is 491 // replaced with a new value before the callback is called. 492 ValueAnimator cachedAnimator = mAppearAnimator; 493 // Even when delay=0, starting the animation on the next frame is necessary to avoid jank. 494 // Not doing so will increase the chances our Animator will be forced to skip a value of 495 // the animation's progression, causing stutter. 496 Choreographer.getInstance().postFrameCallbackDelayed( 497 frameTimeNanos -> { 498 if (mAppearAnimator == cachedAnimator) { 499 mAppearAnimator.start(); 500 } 501 }, delay); 502 } 503 getCujType(boolean isAppearing)504 private int getCujType(boolean isAppearing) { 505 if (mIsHeadsUpAnimation) { 506 return isAppearing 507 ? InteractionJankMonitor.CUJ_NOTIFICATION_HEADS_UP_APPEAR 508 : InteractionJankMonitor.CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR; 509 } else { 510 return isAppearing 511 ? InteractionJankMonitor.CUJ_NOTIFICATION_ADD 512 : InteractionJankMonitor.CUJ_NOTIFICATION_REMOVE; 513 } 514 } 515 onAppearAnimationFinished(boolean wasAppearing)516 protected void onAppearAnimationFinished(boolean wasAppearing) { 517 } 518 cancelAppearAnimation()519 private void cancelAppearAnimation() { 520 if (mAppearAnimator != null) { 521 mAppearAnimator.cancel(); 522 mAppearAnimator = null; 523 } 524 } 525 cancelAppearDrawing()526 public void cancelAppearDrawing() { 527 cancelAppearAnimation(); 528 enableAppearDrawing(false); 529 } 530 531 /** 532 * Update the View's Rect clipping to fit the appear animation 533 * @param clipSide Which side if view we want to clip from 534 */ updateAppearRect(ClipSide clipSide)535 private void updateAppearRect(ClipSide clipSide) { 536 float interpolatedFraction = 537 NotificationsImprovedHunAnimation.isEnabled() 538 || NotificationHeadsUpCycling.isEnabled() ? mAppearAnimationFraction 539 : mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction); 540 mAppearAnimationTranslation = (1.0f - interpolatedFraction) * mAnimationTranslationY; 541 final int fullHeight = getActualHeight(); 542 float height = fullHeight * interpolatedFraction; 543 if (mTargetPoint != null) { 544 int width = getWidth(); 545 float fraction = 1 - mAppearAnimationFraction; 546 547 setOutlineRect(mTargetPoint.x * fraction, 548 mAnimationTranslationY 549 + (mAnimationTranslationY - mTargetPoint.y) * fraction, 550 width - (width - mTargetPoint.x) * fraction, 551 fullHeight - (fullHeight - mTargetPoint.y) * fraction); 552 } else { 553 if (clipSide == TOP) { 554 setOutlineRect( 555 0, 556 /* top= */ fullHeight - height, 557 getWidth(), 558 /* bottom= */ fullHeight 559 ); 560 } else if (clipSide == BOTTOM) { 561 setOutlineRect(0, mAppearAnimationTranslation, getWidth(), 562 height + mAppearAnimationTranslation); 563 } 564 } 565 } 566 updateAppearRect()567 private void updateAppearRect() { 568 updateAppearRect(ClipSide.BOTTOM); 569 } 570 getInterpolatedAppearAnimationFraction()571 private float getInterpolatedAppearAnimationFraction() { 572 573 if (mAppearAnimationFraction >= 0) { 574 return mCurrentAppearInterpolator.getInterpolation(mAppearAnimationFraction); 575 } 576 return 1.0f; 577 } 578 updateAppearAnimationAlpha()579 private void updateAppearAnimationAlpha() { 580 updateAppearAnimationContentAlpha( 581 mAppearAnimationFraction, 582 ALPHA_APPEAR_START_FRACTION, 583 ALPHA_APPEAR_END_FRACTION, 584 Interpolators.ALPHA_IN 585 ); 586 } 587 588 /** 589 * Update the alpha value of the content view during the appear animation. We suppose that the 590 * content alpha changes from 0 to 1 during some part of the appear animation. 591 * @param appearFraction the current appearFraction, should be in the range of [0, 1], where 592 * 1 represents fully appeared 593 * @param startFraction the appear fraction when the content view should be 594 * * fully transparent 595 * @param endFraction the appear fraction when the content view should be 596 * fully in-transparent, should be greater or equals to startFraction 597 * @param interpolator the interpolator to update the alpha 598 */ updateAppearAnimationContentAlpha( float appearFraction, float startFraction, float endFraction, Interpolator interpolator )599 private void updateAppearAnimationContentAlpha( 600 float appearFraction, 601 float startFraction, 602 float endFraction, 603 Interpolator interpolator 604 ) { 605 float contentAlphaProgress = MathUtils.constrain(appearFraction, startFraction, 606 endFraction); 607 float range = endFraction - startFraction; 608 float alpha = (contentAlphaProgress - startFraction) / range; 609 setContentAlpha(interpolator.getInterpolation(alpha)); 610 } 611 setContentAlpha(float contentAlpha)612 private void setContentAlpha(float contentAlpha) { 613 setAlphaAndLayerType(getContentView(), contentAlpha); 614 // After updating the current view, reset all views. 615 if (contentAlpha == 1f) { 616 resetAllContentAlphas(); 617 } 618 } 619 620 /** 621 * Set a content view's alpha value and hardware layer type for fluent animations 622 * @param contentView the view to set 623 * @param alpha the alpha value to set 624 */ setAlphaAndLayerType(View contentView, float alpha)625 protected void setAlphaAndLayerType(View contentView, float alpha) { 626 if (contentView.hasOverlappingRendering()) { 627 int layerType = alpha == 0.0f || alpha == 1.0f ? LAYER_TYPE_NONE : LAYER_TYPE_HARDWARE; 628 contentView.setLayerType(layerType, null); 629 } 630 contentView.setAlpha(alpha); 631 } 632 633 /** 634 * If a subclass's {@link #getContentView()} returns different views depending on state, 635 * this method is an opportunity to reset the alpha of ALL content views, not just the 636 * current one, which may prevent a content view that is temporarily hidden from being reset. 637 * 638 * This should setAlpha(1.0f) and setLayerType(LAYER_TYPE_NONE) for all content views. 639 */ resetAllContentAlphas()640 protected void resetAllContentAlphas() {} 641 642 @Override applyRoundnessAndInvalidate()643 public void applyRoundnessAndInvalidate() { 644 applyBackgroundRoundness(getTopCornerRadius(), getBottomCornerRadius()); 645 super.applyRoundnessAndInvalidate(); 646 } 647 648 @Override getTopCornerRadius()649 public float getTopCornerRadius() { 650 if (NotificationsImprovedHunAnimation.isEnabled()) { 651 return super.getTopCornerRadius(); 652 } 653 654 float fraction = getInterpolatedAppearAnimationFraction(); 655 return MathUtils.lerp(0, super.getTopCornerRadius(), fraction); 656 } 657 658 @Override getBottomCornerRadius()659 public float getBottomCornerRadius() { 660 if (NotificationsImprovedHunAnimation.isEnabled()) { 661 return super.getBottomCornerRadius(); 662 } 663 664 float fraction = getInterpolatedAppearAnimationFraction(); 665 return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction); 666 } 667 applyBackgroundRoundness(float topRadius, float bottomRadius)668 private void applyBackgroundRoundness(float topRadius, float bottomRadius) { 669 mBackgroundNormal.setRadius(topRadius, bottomRadius); 670 } 671 getContentView()672 protected abstract View getContentView(); 673 calculateBgColor()674 public int calculateBgColor() { 675 return calculateBgColor(true /* withTint */, true /* withOverRide */); 676 } 677 678 @Override childNeedsClipping(View child)679 protected boolean childNeedsClipping(View child) { 680 if (child instanceof NotificationBackgroundView && isClippingNeeded()) { 681 return true; 682 } 683 return super.childNeedsClipping(child); 684 } 685 686 /** 687 * @param withTint should a possible tint be factored in? 688 * @param withOverride should the value be interpolated with {@link #mOverrideTint} 689 * @return the calculated background color 690 */ calculateBgColor(boolean withTint, boolean withOverride)691 private int calculateBgColor(boolean withTint, boolean withOverride) { 692 if (withOverride && mOverrideTint != NO_COLOR) { 693 int defaultTint = calculateBgColor(withTint, false); 694 return NotificationUtils.interpolateColors(defaultTint, mOverrideTint, mOverrideAmount); 695 } 696 if (withTint && mBgTint != NO_COLOR) { 697 return mBgTint; 698 } else { 699 return mNormalColor; 700 } 701 } 702 getRippleColor()703 private int getRippleColor() { 704 if (mBgTint != 0) { 705 return mTintedRippleColor; 706 } else { 707 return mNormalRippleColor; 708 } 709 } 710 711 /** 712 * When we draw the appear animation, we render the view in a bitmap and render this bitmap 713 * as a shader of a rect. This call creates the Bitmap and switches the drawing mode, 714 * such that the normal drawing of the views does not happen anymore. 715 * 716 * @param enable Should it be enabled. 717 */ enableAppearDrawing(boolean enable)718 private void enableAppearDrawing(boolean enable) { 719 if (enable != mDrawingAppearAnimation) { 720 mDrawingAppearAnimation = enable; 721 if (!enable) { 722 setContentAlpha(1.0f); 723 mAppearAnimationFraction = -1; 724 setOutlineRect(null); 725 } 726 invalidate(); 727 } 728 } 729 isDrawingAppearAnimation()730 public boolean isDrawingAppearAnimation() { 731 return mDrawingAppearAnimation; 732 } 733 734 @Override dispatchDraw(Canvas canvas)735 protected void dispatchDraw(Canvas canvas) { 736 if (mDrawingAppearAnimation) { 737 canvas.save(); 738 canvas.translate(0, mAppearAnimationTranslation); 739 } 740 super.dispatchDraw(canvas); 741 if (mDrawingAppearAnimation) { 742 canvas.restore(); 743 } 744 } 745 746 @Override setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, int outlineTranslation)747 public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, 748 int outlineTranslation) { 749 boolean hiddenBefore = mShadowHidden; 750 mShadowHidden = shadowIntensity == 0.0f; 751 if (!mShadowHidden || !hiddenBefore) { 752 mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (getTranslationZ() 753 + FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd, 754 outlineTranslation); 755 } 756 } 757 getBackgroundColorWithoutTint()758 public int getBackgroundColorWithoutTint() { 759 return calculateBgColor(false /* withTint */, false /* withOverride */); 760 } 761 getCurrentBackgroundTint()762 public int getCurrentBackgroundTint() { 763 return mCurrentBackgroundTint; 764 } 765 isHeadsUp()766 public boolean isHeadsUp() { 767 return false; 768 } 769 770 @Override getHeadsUpHeightWithoutHeader()771 public int getHeadsUpHeightWithoutHeader() { 772 return getHeight(); 773 } 774 775 /** Mark that this view has been dismissed. */ dismiss(boolean refocusOnDismiss)776 public void dismiss(boolean refocusOnDismiss) { 777 mDismissed = true; 778 mRefocusOnDismiss = refocusOnDismiss; 779 } 780 781 /** Mark that this view is no longer dismissed. */ unDismiss()782 public void unDismiss() { 783 mDismissed = false; 784 } 785 786 /** Is this view marked as dismissed? */ isDismissed()787 public boolean isDismissed() { 788 return mDismissed; 789 } 790 791 /** Should a re-focus occur upon dismissing this view? */ shouldRefocusOnDismiss()792 public boolean shouldRefocusOnDismiss() { 793 return mRefocusOnDismiss || isAccessibilityFocused(); 794 } 795 setTouchHandler(Gefingerpoken touchHandler)796 public void setTouchHandler(Gefingerpoken touchHandler) { 797 mTouchHandler = touchHandler; 798 } 799 800 @Override onDetachedFromWindow()801 protected void onDetachedFromWindow() { 802 super.onDetachedFromWindow(); 803 if (!mOnDetachResetRoundness.isEmpty()) { 804 for (SourceType sourceType : mOnDetachResetRoundness) { 805 requestRoundnessReset(sourceType); 806 } 807 mOnDetachResetRoundness.clear(); 808 } 809 } 810 811 @Override dispatchTouchEvent(MotionEvent ev)812 public boolean dispatchTouchEvent(MotionEvent ev) { 813 return TouchLogger.logDispatchTouch( 814 getClass().getSimpleName(), ev, super.dispatchTouchEvent(ev)); 815 } 816 817 /** 818 * SourceType which should be reset when this View is detached 819 * @param sourceType will be reset on View detached 820 */ addOnDetachResetRoundness(SourceType sourceType)821 public void addOnDetachResetRoundness(SourceType sourceType) { 822 mOnDetachResetRoundness.add(sourceType); 823 } 824 825 @Override dump(PrintWriter pwOriginal, String[] args)826 public void dump(PrintWriter pwOriginal, String[] args) { 827 IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); 828 super.dump(pw, args); 829 if (DUMP_VERBOSE) { 830 DumpUtilsKt.withIncreasedIndent(pw, () -> { 831 dumpBackgroundView(pw, args); 832 }); 833 } 834 } 835 dumpBackgroundView(IndentingPrintWriter pw, String[] args)836 protected void dumpBackgroundView(IndentingPrintWriter pw, String[] args) { 837 pw.println("Background View: " + mBackgroundNormal); 838 if (DUMP_VERBOSE && mBackgroundNormal != null) { 839 DumpUtilsKt.withIncreasedIndent(pw, () -> { 840 mBackgroundNormal.dump(pw, args); 841 }); 842 } 843 } 844 } 845