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