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