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 android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.RectF;
27 import android.util.AttributeSet;
28 import android.util.MathUtils;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.ViewAnimationUtils;
32 import android.view.accessibility.AccessibilityManager;
33 import android.view.animation.Interpolator;
34 import android.view.animation.PathInterpolator;
35 
36 import com.android.systemui.Gefingerpoken;
37 import com.android.systemui.Interpolators;
38 import com.android.systemui.R;
39 import com.android.systemui.statusbar.NotificationShelf;
40 import com.android.systemui.statusbar.notification.FakeShadowView;
41 import com.android.systemui.statusbar.notification.NotificationUtils;
42 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
43 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
44 
45 /**
46  * Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf}
47  * to implement dimming/activating on Keyguard for the double-tap gesture
48  */
49 public abstract class ActivatableNotificationView extends ExpandableOutlineView {
50 
51     private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
52     private static final int ACTIVATE_ANIMATION_LENGTH = 220;
53 
54     /**
55      * The amount of width, which is kept in the end when performing a disappear animation (also
56      * the amount from which the horizontal appearing begins)
57      */
58     private static final float HORIZONTAL_COLLAPSED_REST_PARTIAL = 0.05f;
59 
60     /**
61      * At which point from [0,1] does the horizontal collapse animation end (or start when
62      * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
63      */
64     private static final float HORIZONTAL_ANIMATION_END = 0.2f;
65 
66     /**
67      * At which point from [0,1] does the alpha animation end (or start when
68      * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
69      */
70     private static final float ALPHA_ANIMATION_END = 0.0f;
71 
72     /**
73      * At which point from [0,1] does the horizontal collapse animation start (or start when
74      * expanding)? 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
75      */
76     private static final float HORIZONTAL_ANIMATION_START = 1.0f;
77 
78     /**
79      * At which point from [0,1] does the vertical collapse animation start (or end when
80      * expanding) 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
81      */
82     private static final float VERTICAL_ANIMATION_START = 1.0f;
83 
84     /**
85      * A sentinel value when no color should be used. Can be used with {@link #setTintColor(int)}
86      * or {@link #setOverrideTintColor(int, float)}.
87      */
88     protected static final int NO_COLOR = 0;
89 
90     private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
91             = new PathInterpolator(0.6f, 0, 0.5f, 1);
92     private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
93             = new PathInterpolator(0, 0, 0.5f, 1);
94     private int mTintedRippleColor;
95     private int mNormalRippleColor;
96     private Gefingerpoken mTouchHandler;
97 
98     private boolean mDimmed;
99 
100     int mBgTint = NO_COLOR;
101 
102     /**
103      * Flag to indicate that the notification has been touched once and the second touch will
104      * click it.
105      */
106     private boolean mActivated;
107 
108     private OnActivatedListener mOnActivatedListener;
109 
110     private final Interpolator mSlowOutFastInInterpolator;
111     private final Interpolator mSlowOutLinearInInterpolator;
112     private Interpolator mCurrentAppearInterpolator;
113     private Interpolator mCurrentAlphaInterpolator;
114 
115     NotificationBackgroundView mBackgroundNormal;
116     private NotificationBackgroundView mBackgroundDimmed;
117     private ObjectAnimator mBackgroundAnimator;
118     private RectF mAppearAnimationRect = new RectF();
119     private float mAnimationTranslationY;
120     private boolean mDrawingAppearAnimation;
121     private ValueAnimator mAppearAnimator;
122     private ValueAnimator mBackgroundColorAnimator;
123     private float mAppearAnimationFraction = -1.0f;
124     private float mAppearAnimationTranslation;
125     private int mNormalColor;
126     private boolean mIsBelowSpeedBump;
127 
128     private float mNormalBackgroundVisibilityAmount;
129     private float mDimmedBackgroundFadeInAmount = -1;
130     private ValueAnimator.AnimatorUpdateListener mBackgroundVisibilityUpdater
131             = new ValueAnimator.AnimatorUpdateListener() {
132         @Override
133         public void onAnimationUpdate(ValueAnimator animation) {
134             setNormalBackgroundVisibilityAmount(mBackgroundNormal.getAlpha());
135             mDimmedBackgroundFadeInAmount = mBackgroundDimmed.getAlpha();
136         }
137     };
138     private FakeShadowView mFakeShadow;
139     private int mCurrentBackgroundTint;
140     private int mTargetTint;
141     private int mStartTint;
142     private int mOverrideTint;
143     private float mOverrideAmount;
144     private boolean mShadowHidden;
145     /**
146      * Similar to mDimmed but is also true if it's not dimmable but should be
147      */
148     private boolean mNeedsDimming;
149     private int mDimmedAlpha;
150     private boolean mIsHeadsUpAnimation;
151     private int mHeadsUpAddStartLocation;
152     private float mHeadsUpLocation;
153     private boolean mIsAppearing;
154     private boolean mDismissed;
155     private boolean mRefocusOnDismiss;
156     private OnDimmedListener mOnDimmedListener;
157     private AccessibilityManager mAccessibilityManager;
158 
ActivatableNotificationView(Context context, AttributeSet attrs)159     public ActivatableNotificationView(Context context, AttributeSet attrs) {
160         super(context, attrs);
161         mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
162         mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
163         setClipChildren(false);
164         setClipToPadding(false);
165         updateColors();
166         initDimens();
167     }
168 
updateColors()169     private void updateColors() {
170         mNormalColor = mContext.getColor(R.color.notification_material_background_color);
171         mTintedRippleColor = mContext.getColor(
172                 R.color.notification_ripple_tinted_color);
173         mNormalRippleColor = mContext.getColor(
174                 R.color.notification_ripple_untinted_color);
175         mDimmedAlpha = Color.alpha(mContext.getColor(
176                 R.color.notification_material_background_dimmed_color));
177     }
178 
initDimens()179     private void initDimens() {
180         mHeadsUpAddStartLocation = getResources().getDimensionPixelSize(
181                 com.android.internal.R.dimen.notification_content_margin_start);
182     }
183 
184     @Override
onDensityOrFontScaleChanged()185     public void onDensityOrFontScaleChanged() {
186         super.onDensityOrFontScaleChanged();
187         initDimens();
188     }
189 
updateBackgroundColors()190     protected void updateBackgroundColors() {
191         updateColors();
192         initBackground();
193         updateBackgroundTint();
194     }
195 
196     @Override
onFinishInflate()197     protected void onFinishInflate() {
198         super.onFinishInflate();
199         mBackgroundNormal = findViewById(R.id.backgroundNormal);
200         mFakeShadow = findViewById(R.id.fake_shadow);
201         mShadowHidden = mFakeShadow.getVisibility() != VISIBLE;
202         mBackgroundDimmed = findViewById(R.id.backgroundDimmed);
203         initBackground();
204         updateBackground();
205         updateBackgroundTint();
206         updateOutlineAlpha();
207     }
208 
209     /**
210      * Sets the custom backgrounds on {@link #mBackgroundNormal} and {@link #mBackgroundDimmed}.
211      * This method can also be used to reload the backgrounds on both of those views, which can
212      * be useful in a configuration change.
213      */
initBackground()214     protected void initBackground() {
215         mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg);
216         mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim);
217     }
218 
219 
220     @Override
onInterceptTouchEvent(MotionEvent ev)221     public boolean onInterceptTouchEvent(MotionEvent ev) {
222         if (mTouchHandler != null && mTouchHandler.onInterceptTouchEvent(ev)) {
223             return true;
224         }
225         return super.onInterceptTouchEvent(ev);
226     }
227 
disallowSingleClick(MotionEvent ev)228     protected boolean disallowSingleClick(MotionEvent ev) {
229         return false;
230     }
231 
handleSlideBack()232     protected boolean handleSlideBack() {
233         return false;
234     }
235 
236     /**
237      * @return whether this view is interactive and can be double tapped
238      */
isInteractive()239     protected boolean isInteractive() {
240         return true;
241     }
242 
243     @Override
drawableHotspotChanged(float x, float y)244     public void drawableHotspotChanged(float x, float y) {
245         if (!mDimmed){
246             mBackgroundNormal.drawableHotspotChanged(x, y);
247         }
248     }
249 
250     @Override
drawableStateChanged()251     protected void drawableStateChanged() {
252         super.drawableStateChanged();
253         if (mDimmed) {
254             mBackgroundDimmed.setState(getDrawableState());
255         } else {
256             mBackgroundNormal.setState(getDrawableState());
257         }
258     }
259 
setRippleAllowed(boolean allowed)260     void setRippleAllowed(boolean allowed) {
261         mBackgroundNormal.setPressedAllowed(allowed);
262     }
263 
makeActive()264     void makeActive() {
265         startActivateAnimation(false /* reverse */);
266         mActivated = true;
267         if (mOnActivatedListener != null) {
268             mOnActivatedListener.onActivated(this);
269         }
270     }
271 
isActive()272     public boolean isActive() {
273         return mActivated;
274     }
275 
startActivateAnimation(final boolean reverse)276     private void startActivateAnimation(final boolean reverse) {
277         if (!isAttachedToWindow()) {
278             return;
279         }
280         if (!isDimmable()) {
281             return;
282         }
283         int widthHalf = mBackgroundNormal.getWidth()/2;
284         int heightHalf = mBackgroundNormal.getActualHeight()/2;
285         float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf);
286         Animator animator;
287         if (reverse) {
288             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
289                     widthHalf, heightHalf, radius, 0);
290         } else {
291             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
292                     widthHalf, heightHalf, 0, radius);
293         }
294         mBackgroundNormal.setVisibility(View.VISIBLE);
295         Interpolator interpolator;
296         Interpolator alphaInterpolator;
297         if (!reverse) {
298             interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
299             alphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
300         } else {
301             interpolator = ACTIVATE_INVERSE_INTERPOLATOR;
302             alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR;
303         }
304         animator.setInterpolator(interpolator);
305         animator.setDuration(ACTIVATE_ANIMATION_LENGTH);
306         if (reverse) {
307             mBackgroundNormal.setAlpha(1f);
308             animator.addListener(new AnimatorListenerAdapter() {
309                 @Override
310                 public void onAnimationEnd(Animator animation) {
311                     updateBackground();
312                 }
313             });
314             animator.start();
315         } else {
316             mBackgroundNormal.setAlpha(0.4f);
317             animator.start();
318         }
319         mBackgroundNormal.animate()
320                 .alpha(reverse ? 0f : 1f)
321                 .setInterpolator(alphaInterpolator)
322                 .setUpdateListener(animation -> {
323                     float animatedFraction = animation.getAnimatedFraction();
324                     if (reverse) {
325                         animatedFraction = 1.0f - animatedFraction;
326                     }
327                     setNormalBackgroundVisibilityAmount(animatedFraction);
328                 })
329                 .setDuration(ACTIVATE_ANIMATION_LENGTH);
330     }
331 
332     /**
333      * Cancels the hotspot and makes the notification inactive.
334      */
makeInactive(boolean animate)335     public void makeInactive(boolean animate) {
336         if (mActivated) {
337             mActivated = false;
338             if (mDimmed) {
339                 if (animate) {
340                     startActivateAnimation(true /* reverse */);
341                 } else {
342                     updateBackground();
343                 }
344             }
345         }
346         if (mOnActivatedListener != null) {
347             mOnActivatedListener.onActivationReset(this);
348         }
349     }
350 
setDimmed(boolean dimmed, boolean fade)351     public void setDimmed(boolean dimmed, boolean fade) {
352         mNeedsDimming = dimmed;
353         if (mOnDimmedListener != null) {
354             mOnDimmedListener.onSetDimmed(dimmed);
355         }
356         dimmed &= isDimmable();
357         if (mDimmed != dimmed) {
358             mDimmed = dimmed;
359             resetBackgroundAlpha();
360             if (fade) {
361                 fadeDimmedBackground();
362             } else {
363                 updateBackground();
364             }
365         }
366     }
367 
isDimmable()368     public boolean isDimmable() {
369         return true;
370     }
371 
isDimmed()372     public boolean isDimmed() {
373         return mDimmed;
374     }
375 
updateOutlineAlpha()376     private void updateOutlineAlpha() {
377         float alpha = NotificationStackScrollLayout.BACKGROUND_ALPHA_DIMMED;
378         alpha = (alpha + (1.0f - alpha) * mNormalBackgroundVisibilityAmount);
379         setOutlineAlpha(alpha);
380     }
381 
setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount)382     private void setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount) {
383         mNormalBackgroundVisibilityAmount = normalBackgroundVisibilityAmount;
384         updateOutlineAlpha();
385     }
386 
387     @Override
setBelowSpeedBump(boolean below)388     public void setBelowSpeedBump(boolean below) {
389         super.setBelowSpeedBump(below);
390         if (below != mIsBelowSpeedBump) {
391             mIsBelowSpeedBump = below;
392             updateBackgroundTint();
393             onBelowSpeedBumpChanged();
394         }
395     }
396 
onBelowSpeedBumpChanged()397     protected void onBelowSpeedBumpChanged() {
398     }
399 
400     /**
401      * @return whether we are below the speed bump
402      */
isBelowSpeedBump()403     public boolean isBelowSpeedBump() {
404         return mIsBelowSpeedBump;
405     }
406 
407     /**
408      * Sets the tint color of the background
409      */
setTintColor(int color)410     protected void setTintColor(int color) {
411         setTintColor(color, false);
412     }
413 
414     /**
415      * Sets the tint color of the background
416      */
setTintColor(int color, boolean animated)417     void setTintColor(int color, boolean animated) {
418         if (color != mBgTint) {
419             mBgTint = color;
420             updateBackgroundTint(animated);
421         }
422     }
423 
424     @Override
setDistanceToTopRoundness(float distanceToTopRoundness)425     public void setDistanceToTopRoundness(float distanceToTopRoundness) {
426         super.setDistanceToTopRoundness(distanceToTopRoundness);
427         mBackgroundNormal.setDistanceToTopRoundness(distanceToTopRoundness);
428         mBackgroundDimmed.setDistanceToTopRoundness(distanceToTopRoundness);
429     }
430 
431     /** Sets whether this view is the last notification in a section. */
432     @Override
setLastInSection(boolean lastInSection)433     public void setLastInSection(boolean lastInSection) {
434         if (lastInSection != mLastInSection) {
435             super.setLastInSection(lastInSection);
436             mBackgroundNormal.setLastInSection(lastInSection);
437             mBackgroundDimmed.setLastInSection(lastInSection);
438         }
439     }
440 
441     /** Sets whether this view is the first notification in a section. */
442     @Override
setFirstInSection(boolean firstInSection)443     public void setFirstInSection(boolean firstInSection) {
444         if (firstInSection != mFirstInSection) {
445             super.setFirstInSection(firstInSection);
446             mBackgroundNormal.setFirstInSection(firstInSection);
447             mBackgroundDimmed.setFirstInSection(firstInSection);
448         }
449     }
450 
451     /**
452      * Set an override tint color that is used for the background.
453      *
454      * @param color the color that should be used to tint the background.
455      *              This can be {@link #NO_COLOR} if the tint should be normally computed.
456      * @param overrideAmount a value from 0 to 1 how much the override tint should be used. The
457      *                       background color will then be the interpolation between this and the
458      *                       regular background color, where 1 means the overrideTintColor is fully
459      *                       used and the background color not at all.
460      */
setOverrideTintColor(int color, float overrideAmount)461     public void setOverrideTintColor(int color, float overrideAmount) {
462         mOverrideTint = color;
463         mOverrideAmount = overrideAmount;
464         int newColor = calculateBgColor();
465         setBackgroundTintColor(newColor);
466         if (!isDimmable() && mNeedsDimming) {
467            mBackgroundNormal.setDrawableAlpha((int) NotificationUtils.interpolate(255,
468                    mDimmedAlpha,
469                    overrideAmount));
470         } else {
471             mBackgroundNormal.setDrawableAlpha(255);
472         }
473     }
474 
updateBackgroundTint()475     protected void updateBackgroundTint() {
476         updateBackgroundTint(false /* animated */);
477     }
478 
updateBackgroundTint(boolean animated)479     private void updateBackgroundTint(boolean animated) {
480         if (mBackgroundColorAnimator != null) {
481             mBackgroundColorAnimator.cancel();
482         }
483         int rippleColor = getRippleColor();
484         mBackgroundDimmed.setRippleColor(rippleColor);
485         mBackgroundNormal.setRippleColor(rippleColor);
486         int color = calculateBgColor();
487         if (!animated) {
488             setBackgroundTintColor(color);
489         } else if (color != mCurrentBackgroundTint) {
490             mStartTint = mCurrentBackgroundTint;
491             mTargetTint = color;
492             mBackgroundColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
493             mBackgroundColorAnimator.addUpdateListener(animation -> {
494                 int newColor = NotificationUtils.interpolateColors(mStartTint, mTargetTint,
495                         animation.getAnimatedFraction());
496                 setBackgroundTintColor(newColor);
497             });
498             mBackgroundColorAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
499             mBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR);
500             mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() {
501                 @Override
502                 public void onAnimationEnd(Animator animation) {
503                     mBackgroundColorAnimator = null;
504                 }
505             });
506             mBackgroundColorAnimator.start();
507         }
508     }
509 
setBackgroundTintColor(int color)510     protected void setBackgroundTintColor(int color) {
511         if (color != mCurrentBackgroundTint) {
512             mCurrentBackgroundTint = color;
513             if (color == mNormalColor) {
514                 // We don't need to tint a normal notification
515                 color = 0;
516             }
517             mBackgroundDimmed.setTint(color);
518             mBackgroundNormal.setTint(color);
519         }
520     }
521 
522     /**
523      * Fades the background when the dimmed state changes.
524      */
fadeDimmedBackground()525     private void fadeDimmedBackground() {
526         mBackgroundDimmed.animate().cancel();
527         mBackgroundNormal.animate().cancel();
528         if (mActivated) {
529             updateBackground();
530             return;
531         }
532         if (!shouldHideBackground()) {
533             if (mDimmed) {
534                 mBackgroundDimmed.setVisibility(View.VISIBLE);
535             } else {
536                 mBackgroundNormal.setVisibility(View.VISIBLE);
537             }
538         }
539         float startAlpha = mDimmed ? 1f : 0;
540         float endAlpha = mDimmed ? 0 : 1f;
541         int duration = BACKGROUND_ANIMATION_LENGTH_MS;
542         // Check whether there is already a background animation running.
543         if (mBackgroundAnimator != null) {
544             startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
545             duration = (int) mBackgroundAnimator.getCurrentPlayTime();
546             mBackgroundAnimator.removeAllListeners();
547             mBackgroundAnimator.cancel();
548             if (duration <= 0) {
549                 updateBackground();
550                 return;
551             }
552         }
553         mBackgroundNormal.setAlpha(startAlpha);
554         mBackgroundAnimator =
555                 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
556         mBackgroundAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
557         mBackgroundAnimator.setDuration(duration);
558         mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
559             @Override
560             public void onAnimationEnd(Animator animation) {
561                 updateBackground();
562                 mBackgroundAnimator = null;
563                 mDimmedBackgroundFadeInAmount = -1;
564             }
565         });
566         mBackgroundAnimator.addUpdateListener(mBackgroundVisibilityUpdater);
567         mBackgroundAnimator.start();
568     }
569 
updateBackgroundAlpha(float transformationAmount)570     protected void updateBackgroundAlpha(float transformationAmount) {
571         float bgAlpha = isChildInGroup() && mDimmed ? transformationAmount : 1f;
572         if (mDimmedBackgroundFadeInAmount != -1) {
573             bgAlpha *= mDimmedBackgroundFadeInAmount;
574         }
575         mBackgroundDimmed.setAlpha(bgAlpha);
576     }
577 
resetBackgroundAlpha()578     protected void resetBackgroundAlpha() {
579         updateBackgroundAlpha(0f /* transformationAmount */);
580     }
581 
updateBackground()582     protected void updateBackground() {
583         cancelFadeAnimations();
584         if (shouldHideBackground()) {
585             mBackgroundDimmed.setVisibility(INVISIBLE);
586             mBackgroundNormal.setVisibility(mActivated ? VISIBLE : INVISIBLE);
587         } else if (mDimmed) {
588             // When groups are animating to the expanded state from the lockscreen, show the
589             // normal background instead of the dimmed background.
590             final boolean dontShowDimmed = isGroupExpansionChanging() && isChildInGroup();
591             mBackgroundDimmed.setVisibility(dontShowDimmed ? View.INVISIBLE : View.VISIBLE);
592             mBackgroundNormal.setVisibility((mActivated || dontShowDimmed)
593                     ? View.VISIBLE
594                     : View.INVISIBLE);
595         } else {
596             mBackgroundDimmed.setVisibility(View.INVISIBLE);
597             mBackgroundNormal.setVisibility(View.VISIBLE);
598             mBackgroundNormal.setAlpha(1f);
599             // make in inactive to avoid it sticking around active
600             makeInactive(false /* animate */);
601         }
602         setNormalBackgroundVisibilityAmount(
603                 mBackgroundNormal.getVisibility() == View.VISIBLE ? 1.0f : 0.0f);
604     }
605 
updateBackgroundClipping()606     protected void updateBackgroundClipping() {
607         mBackgroundNormal.setBottomAmountClips(!isChildInGroup());
608         mBackgroundDimmed.setBottomAmountClips(!isChildInGroup());
609     }
610 
shouldHideBackground()611     protected boolean shouldHideBackground() {
612         return false;
613     }
614 
cancelFadeAnimations()615     private void cancelFadeAnimations() {
616         if (mBackgroundAnimator != null) {
617             mBackgroundAnimator.cancel();
618         }
619         mBackgroundDimmed.animate().cancel();
620         mBackgroundNormal.animate().cancel();
621     }
622 
623     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)624     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
625         super.onLayout(changed, left, top, right, bottom);
626         setPivotX(getWidth() / 2);
627     }
628 
629     @Override
setActualHeight(int actualHeight, boolean notifyListeners)630     public void setActualHeight(int actualHeight, boolean notifyListeners) {
631         super.setActualHeight(actualHeight, notifyListeners);
632         setPivotY(actualHeight / 2);
633         mBackgroundNormal.setActualHeight(actualHeight);
634         mBackgroundDimmed.setActualHeight(actualHeight);
635     }
636 
637     @Override
setClipTopAmount(int clipTopAmount)638     public void setClipTopAmount(int clipTopAmount) {
639         super.setClipTopAmount(clipTopAmount);
640         mBackgroundNormal.setClipTopAmount(clipTopAmount);
641         mBackgroundDimmed.setClipTopAmount(clipTopAmount);
642     }
643 
644     @Override
setClipBottomAmount(int clipBottomAmount)645     public void setClipBottomAmount(int clipBottomAmount) {
646         super.setClipBottomAmount(clipBottomAmount);
647         mBackgroundNormal.setClipBottomAmount(clipBottomAmount);
648         mBackgroundDimmed.setClipBottomAmount(clipBottomAmount);
649     }
650 
651     @Override
performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)652     public long performRemoveAnimation(long duration, long delay,
653             float translationDirection, boolean isHeadsUpAnimation, float endLocation,
654             Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener) {
655         enableAppearDrawing(true);
656         mIsHeadsUpAnimation = isHeadsUpAnimation;
657         mHeadsUpLocation = endLocation;
658         if (mDrawingAppearAnimation) {
659             startAppearAnimation(false /* isAppearing */, translationDirection,
660                     delay, duration, onFinishedRunnable, animationListener);
661         } else if (onFinishedRunnable != null) {
662             onFinishedRunnable.run();
663         }
664         return 0;
665     }
666 
667     @Override
performAddAnimation(long delay, long duration, boolean isHeadsUpAppear)668     public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) {
669         enableAppearDrawing(true);
670         mIsHeadsUpAnimation = isHeadsUpAppear;
671         mHeadsUpLocation = mHeadsUpAddStartLocation;
672         if (mDrawingAppearAnimation) {
673             startAppearAnimation(true /* isAppearing */, isHeadsUpAppear ? 0.0f : -1.0f, delay,
674                     duration, null, null);
675         }
676     }
677 
startAppearAnimation(boolean isAppearing, float translationDirection, long delay, long duration, final Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)678     private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay,
679             long duration, final Runnable onFinishedRunnable,
680             AnimatorListenerAdapter animationListener) {
681         cancelAppearAnimation();
682         mAnimationTranslationY = translationDirection * getActualHeight();
683         if (mAppearAnimationFraction == -1.0f) {
684             // not initialized yet, we start anew
685             if (isAppearing) {
686                 mAppearAnimationFraction = 0.0f;
687                 mAppearAnimationTranslation = mAnimationTranslationY;
688             } else {
689                 mAppearAnimationFraction = 1.0f;
690                 mAppearAnimationTranslation = 0;
691             }
692         }
693         mIsAppearing = isAppearing;
694 
695         float targetValue;
696         if (isAppearing) {
697             mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
698             mCurrentAlphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
699             targetValue = 1.0f;
700         } else {
701             mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN;
702             mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
703             targetValue = 0.0f;
704         }
705         mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
706                 targetValue);
707         mAppearAnimator.setInterpolator(Interpolators.LINEAR);
708         mAppearAnimator.setDuration(
709                 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
710         mAppearAnimator.addUpdateListener(animation -> {
711             mAppearAnimationFraction = (float) animation.getAnimatedValue();
712             updateAppearAnimationAlpha();
713             updateAppearRect();
714             invalidate();
715         });
716         if (animationListener != null) {
717             mAppearAnimator.addListener(animationListener);
718         }
719         if (delay > 0) {
720             // we need to apply the initial state already to avoid drawn frames in the wrong state
721             updateAppearAnimationAlpha();
722             updateAppearRect();
723             mAppearAnimator.setStartDelay(delay);
724         }
725         mAppearAnimator.addListener(new AnimatorListenerAdapter() {
726             private boolean mWasCancelled;
727 
728             @Override
729             public void onAnimationEnd(Animator animation) {
730                 if (onFinishedRunnable != null) {
731                     onFinishedRunnable.run();
732                 }
733                 if (!mWasCancelled) {
734                     enableAppearDrawing(false);
735                     onAppearAnimationFinished(isAppearing);
736                 }
737             }
738 
739             @Override
740             public void onAnimationStart(Animator animation) {
741                 mWasCancelled = false;
742             }
743 
744             @Override
745             public void onAnimationCancel(Animator animation) {
746                 mWasCancelled = true;
747             }
748         });
749         mAppearAnimator.start();
750     }
751 
onAppearAnimationFinished(boolean wasAppearing)752     protected void onAppearAnimationFinished(boolean wasAppearing) {
753     }
754 
cancelAppearAnimation()755     private void cancelAppearAnimation() {
756         if (mAppearAnimator != null) {
757             mAppearAnimator.cancel();
758             mAppearAnimator = null;
759         }
760     }
761 
cancelAppearDrawing()762     public void cancelAppearDrawing() {
763         cancelAppearAnimation();
764         enableAppearDrawing(false);
765     }
766 
updateAppearRect()767     private void updateAppearRect() {
768         float inverseFraction = (1.0f - mAppearAnimationFraction);
769         float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
770         float translateYTotalAmount = translationFraction * mAnimationTranslationY;
771         mAppearAnimationTranslation = translateYTotalAmount;
772 
773         // handle width animation
774         float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
775                 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
776         widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
777         widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
778         float startWidthFraction = HORIZONTAL_COLLAPSED_REST_PARTIAL;
779         if (mIsHeadsUpAnimation && !mIsAppearing) {
780             startWidthFraction = 0;
781         }
782         float width = MathUtils.lerp(startWidthFraction, 1.0f, 1.0f - widthFraction)
783                         * getWidth();
784         float left;
785         float right;
786         if (mIsHeadsUpAnimation) {
787             left = MathUtils.lerp(mHeadsUpLocation, 0, 1.0f - widthFraction);
788             right = left + width;
789         } else {
790             left = getWidth() * 0.5f - width / 2.0f;
791             right = getWidth() - left;
792         }
793 
794         // handle top animation
795         float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
796                 VERTICAL_ANIMATION_START;
797         heightFraction = Math.max(0.0f, heightFraction);
798         heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
799 
800         float top;
801         float bottom;
802         final int actualHeight = getActualHeight();
803         if (mAnimationTranslationY > 0.0f) {
804             bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f
805                     - translateYTotalAmount;
806             top = bottom * heightFraction;
807         } else {
808             top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f -
809                     translateYTotalAmount;
810             bottom = actualHeight * (1 - heightFraction) + top * heightFraction;
811         }
812         mAppearAnimationRect.set(left, top, right, bottom);
813         setOutlineRect(left, top + mAppearAnimationTranslation, right,
814                 bottom + mAppearAnimationTranslation);
815     }
816 
updateAppearAnimationAlpha()817     private void updateAppearAnimationAlpha() {
818         float contentAlphaProgress = mAppearAnimationFraction;
819         contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
820         contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
821         contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
822         setContentAlpha(contentAlphaProgress);
823     }
824 
setContentAlpha(float contentAlpha)825     private void setContentAlpha(float contentAlpha) {
826         View contentView = getContentView();
827         if (contentView.hasOverlappingRendering()) {
828             int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE
829                     : LAYER_TYPE_HARDWARE;
830             int currentLayerType = contentView.getLayerType();
831             if (currentLayerType != layerType) {
832                 contentView.setLayerType(layerType, null);
833             }
834         }
835         contentView.setAlpha(contentAlpha);
836     }
837 
838     @Override
applyRoundness()839     protected void applyRoundness() {
840         super.applyRoundness();
841         applyBackgroundRoundness(getCurrentBackgroundRadiusTop(),
842                 getCurrentBackgroundRadiusBottom());
843     }
844 
applyBackgroundRoundness(float topRadius, float bottomRadius)845     private void applyBackgroundRoundness(float topRadius, float bottomRadius) {
846         mBackgroundDimmed.setRoundness(topRadius, bottomRadius);
847         mBackgroundNormal.setRoundness(topRadius, bottomRadius);
848     }
849 
850     @Override
setBackgroundTop(int backgroundTop)851     protected void setBackgroundTop(int backgroundTop) {
852         mBackgroundDimmed.setBackgroundTop(backgroundTop);
853         mBackgroundNormal.setBackgroundTop(backgroundTop);
854     }
855 
getContentView()856     protected abstract View getContentView();
857 
calculateBgColor()858     public int calculateBgColor() {
859         return calculateBgColor(true /* withTint */, true /* withOverRide */);
860     }
861 
862     @Override
childNeedsClipping(View child)863     protected boolean childNeedsClipping(View child) {
864         if (child instanceof NotificationBackgroundView && isClippingNeeded()) {
865             return true;
866         }
867         return super.childNeedsClipping(child);
868     }
869 
870     /**
871      * @param withTint should a possible tint be factored in?
872      * @param withOverride should the value be interpolated with {@link #mOverrideTint}
873      * @return the calculated background color
874      */
calculateBgColor(boolean withTint, boolean withOverride)875     private int calculateBgColor(boolean withTint, boolean withOverride) {
876         if (withOverride && mOverrideTint != NO_COLOR) {
877             int defaultTint = calculateBgColor(withTint, false);
878             return NotificationUtils.interpolateColors(defaultTint, mOverrideTint, mOverrideAmount);
879         }
880         if (withTint && mBgTint != NO_COLOR) {
881             return mBgTint;
882         } else {
883             return mNormalColor;
884         }
885     }
886 
getRippleColor()887     private int getRippleColor() {
888         if (mBgTint != 0) {
889             return mTintedRippleColor;
890         } else {
891             return mNormalRippleColor;
892         }
893     }
894 
895     /**
896      * When we draw the appear animation, we render the view in a bitmap and render this bitmap
897      * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
898      * such that the normal drawing of the views does not happen anymore.
899      *
900      * @param enable Should it be enabled.
901      */
enableAppearDrawing(boolean enable)902     private void enableAppearDrawing(boolean enable) {
903         if (enable != mDrawingAppearAnimation) {
904             mDrawingAppearAnimation = enable;
905             if (!enable) {
906                 setContentAlpha(1.0f);
907                 mAppearAnimationFraction = -1;
908                 setOutlineRect(null);
909             }
910             invalidate();
911         }
912     }
913 
isDrawingAppearAnimation()914     public boolean isDrawingAppearAnimation() {
915         return mDrawingAppearAnimation;
916     }
917 
918     @Override
dispatchDraw(Canvas canvas)919     protected void dispatchDraw(Canvas canvas) {
920         if (mDrawingAppearAnimation) {
921             canvas.save();
922             canvas.translate(0, mAppearAnimationTranslation);
923         }
924         super.dispatchDraw(canvas);
925         if (mDrawingAppearAnimation) {
926             canvas.restore();
927         }
928     }
929 
setOnActivatedListener(OnActivatedListener onActivatedListener)930     public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
931         mOnActivatedListener = onActivatedListener;
932     }
933 
934     @Override
setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, int outlineTranslation)935     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
936             int outlineTranslation) {
937         boolean hiddenBefore = mShadowHidden;
938         mShadowHidden = shadowIntensity == 0.0f;
939         if (!mShadowHidden || !hiddenBefore) {
940             mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (getTranslationZ()
941                             + FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd,
942                     outlineTranslation);
943         }
944     }
945 
getBackgroundColorWithoutTint()946     public int getBackgroundColorWithoutTint() {
947         return calculateBgColor(false /* withTint */, false /* withOverride */);
948     }
949 
getCurrentBackgroundTint()950     public int getCurrentBackgroundTint() {
951         return mCurrentBackgroundTint;
952     }
953 
isHeadsUp()954     public boolean isHeadsUp() {
955         return false;
956     }
957 
958     @Override
getHeadsUpHeightWithoutHeader()959     public int getHeadsUpHeightWithoutHeader() {
960         return getHeight();
961     }
962 
963     /** Mark that this view has been dismissed. */
dismiss(boolean refocusOnDismiss)964     public void dismiss(boolean refocusOnDismiss) {
965         mDismissed = true;
966         mRefocusOnDismiss = refocusOnDismiss;
967     }
968 
969     /** Mark that this view is no longer dismissed. */
unDismiss()970     public void unDismiss() {
971         mDismissed = false;
972     }
973 
974     /** Is this view marked as dismissed? */
isDismissed()975     public boolean isDismissed() {
976         return mDismissed;
977     }
978 
979     /** Should a re-focus occur upon dismissing this view? */
shouldRefocusOnDismiss()980     public boolean shouldRefocusOnDismiss() {
981         return mRefocusOnDismiss || isAccessibilityFocused();
982     }
983 
setTouchHandler(Gefingerpoken touchHandler)984     void setTouchHandler(Gefingerpoken touchHandler) {
985         mTouchHandler = touchHandler;
986     }
987 
setOnDimmedListener(OnDimmedListener onDimmedListener)988     void setOnDimmedListener(OnDimmedListener onDimmedListener) {
989         mOnDimmedListener = onDimmedListener;
990     }
991 
setAccessibilityManager(AccessibilityManager accessibilityManager)992     public void setAccessibilityManager(AccessibilityManager accessibilityManager) {
993         mAccessibilityManager = accessibilityManager;
994     }
995 
996     public interface OnActivatedListener {
onActivated(ActivatableNotificationView view)997         void onActivated(ActivatableNotificationView view);
onActivationReset(ActivatableNotificationView view)998         void onActivationReset(ActivatableNotificationView view);
999     }
1000 
1001     interface OnDimmedListener {
onSetDimmed(boolean dimmed)1002         void onSetDimmed(boolean dimmed);
1003     }
1004 }
1005