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