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