• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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