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