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.ValueAnimator;
23 import android.content.Context;
24 import android.graphics.Canvas;
25 import android.graphics.RectF;
26 import android.util.AttributeSet;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.ViewAnimationUtils;
30 import android.view.ViewConfiguration;
31 import android.view.animation.AnimationUtils;
32 import android.view.animation.Interpolator;
33 import android.view.animation.LinearInterpolator;
34 import android.view.animation.PathInterpolator;
35 
36 import com.android.systemui.R;
37 
38 /**
39  * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer}
40  * to implement dimming/activating on Keyguard for the double-tap gesture
41  */
42 public abstract class ActivatableNotificationView extends ExpandableOutlineView {
43 
44     private static final long DOUBLETAP_TIMEOUT_MS = 1200;
45     private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
46     private static final int ACTIVATE_ANIMATION_LENGTH = 220;
47     private static final int DARK_ANIMATION_LENGTH = 170;
48 
49     /**
50      * The amount of width, which is kept in the end when performing a disappear animation (also
51      * the amount from which the horizontal appearing begins)
52      */
53     private static final float HORIZONTAL_COLLAPSED_REST_PARTIAL = 0.05f;
54 
55     /**
56      * At which point from [0,1] does the horizontal collapse animation end (or start when
57      * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
58      */
59     private static final float HORIZONTAL_ANIMATION_END = 0.2f;
60 
61     /**
62      * At which point from [0,1] does the alpha animation end (or start when
63      * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated.
64      */
65     private static final float ALPHA_ANIMATION_END = 0.0f;
66 
67     /**
68      * At which point from [0,1] does the horizontal collapse animation start (or start when
69      * expanding)? 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
70      */
71     private static final float HORIZONTAL_ANIMATION_START = 1.0f;
72 
73     /**
74      * At which point from [0,1] does the vertical collapse animation start (or end when
75      * expanding) 1.0 meaning that it starts immediately and 0.0 that it is animated at all.
76      */
77     private static final float VERTICAL_ANIMATION_START = 1.0f;
78 
79     /**
80      * Scale for the background to animate from when exiting dark mode.
81      */
82     private static final float DARK_EXIT_SCALE_START = 0.93f;
83 
84     private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
85             = new PathInterpolator(0.6f, 0, 0.5f, 1);
86     private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
87             = new PathInterpolator(0, 0, 0.5f, 1);
88     private final int mTintedRippleColor;
89     private final int mLowPriorityRippleColor;
90     protected final int mNormalRippleColor;
91 
92     private boolean mDimmed;
93     private boolean mDark;
94 
95     private int mBgTint = 0;
96 
97     /**
98      * Flag to indicate that the notification has been touched once and the second touch will
99      * click it.
100      */
101     private boolean mActivated;
102 
103     private float mDownX;
104     private float mDownY;
105     private final float mTouchSlop;
106 
107     private OnActivatedListener mOnActivatedListener;
108 
109     private final Interpolator mLinearOutSlowInInterpolator;
110     protected final Interpolator mFastOutSlowInInterpolator;
111     private final Interpolator mSlowOutFastInInterpolator;
112     private final Interpolator mSlowOutLinearInInterpolator;
113     private final Interpolator mLinearInterpolator;
114     private Interpolator mCurrentAppearInterpolator;
115     private Interpolator mCurrentAlphaInterpolator;
116 
117     private NotificationBackgroundView mBackgroundNormal;
118     private NotificationBackgroundView mBackgroundDimmed;
119     private ObjectAnimator mBackgroundAnimator;
120     private RectF mAppearAnimationRect = new RectF();
121     private float mAnimationTranslationY;
122     private boolean mDrawingAppearAnimation;
123     private ValueAnimator mAppearAnimator;
124     private float mAppearAnimationFraction = -1.0f;
125     private float mAppearAnimationTranslation;
126     private boolean mShowingLegacyBackground;
127     private final int mLegacyColor;
128     private final int mNormalColor;
129     private final int mLowPriorityColor;
130     private boolean mIsBelowSpeedBump;
131 
ActivatableNotificationView(Context context, AttributeSet attrs)132     public ActivatableNotificationView(Context context, AttributeSet attrs) {
133         super(context, attrs);
134         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
135         mFastOutSlowInInterpolator =
136                 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
137         mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
138         mLinearOutSlowInInterpolator =
139                 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
140         mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
141         mLinearInterpolator = new LinearInterpolator();
142         setClipChildren(false);
143         setClipToPadding(false);
144         mLegacyColor = context.getColor(R.color.notification_legacy_background_color);
145         mNormalColor = context.getColor(R.color.notification_material_background_color);
146         mLowPriorityColor = context.getColor(
147                 R.color.notification_material_background_low_priority_color);
148         mTintedRippleColor = context.getColor(
149                 R.color.notification_ripple_tinted_color);
150         mLowPriorityRippleColor = context.getColor(
151                 R.color.notification_ripple_color_low_priority);
152         mNormalRippleColor = context.getColor(
153                 R.color.notification_ripple_untinted_color);
154     }
155 
156     @Override
onFinishInflate()157     protected void onFinishInflate() {
158         super.onFinishInflate();
159         mBackgroundNormal = (NotificationBackgroundView) findViewById(R.id.backgroundNormal);
160         mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed);
161         mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg);
162         mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim);
163         updateBackground();
164         updateBackgroundTint();
165     }
166 
167     private final Runnable mTapTimeoutRunnable = new Runnable() {
168         @Override
169         public void run() {
170             makeInactive(true /* animate */);
171         }
172     };
173 
174     @Override
onTouchEvent(MotionEvent event)175     public boolean onTouchEvent(MotionEvent event) {
176         if (mDimmed) {
177             return handleTouchEventDimmed(event);
178         } else {
179             return super.onTouchEvent(event);
180         }
181     }
182 
183     @Override
drawableHotspotChanged(float x, float y)184     public void drawableHotspotChanged(float x, float y) {
185         if (!mDimmed){
186             mBackgroundNormal.drawableHotspotChanged(x, y);
187         }
188     }
189 
190     @Override
drawableStateChanged()191     protected void drawableStateChanged() {
192         super.drawableStateChanged();
193         if (mDimmed) {
194             mBackgroundDimmed.setState(getDrawableState());
195         } else {
196             mBackgroundNormal.setState(getDrawableState());
197         }
198     }
199 
handleTouchEventDimmed(MotionEvent event)200     private boolean handleTouchEventDimmed(MotionEvent event) {
201         int action = event.getActionMasked();
202         switch (action) {
203             case MotionEvent.ACTION_DOWN:
204                 mDownX = event.getX();
205                 mDownY = event.getY();
206                 if (mDownY > getActualHeight()) {
207                     return false;
208                 }
209                 break;
210             case MotionEvent.ACTION_MOVE:
211                 if (!isWithinTouchSlop(event)) {
212                     makeInactive(true /* animate */);
213                     return false;
214                 }
215                 break;
216             case MotionEvent.ACTION_UP:
217                 if (isWithinTouchSlop(event)) {
218                     if (!mActivated) {
219                         makeActive();
220                         postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
221                     } else {
222                         boolean performed = performClick();
223                         if (performed) {
224                             removeCallbacks(mTapTimeoutRunnable);
225                         }
226                     }
227                 } else {
228                     makeInactive(true /* animate */);
229                 }
230                 break;
231             case MotionEvent.ACTION_CANCEL:
232                 makeInactive(true /* animate */);
233                 break;
234             default:
235                 break;
236         }
237         return true;
238     }
239 
makeActive()240     private void makeActive() {
241         startActivateAnimation(false /* reverse */);
242         mActivated = true;
243         if (mOnActivatedListener != null) {
244             mOnActivatedListener.onActivated(this);
245         }
246     }
247 
startActivateAnimation(boolean reverse)248     private void startActivateAnimation(boolean reverse) {
249         if (!isAttachedToWindow()) {
250             return;
251         }
252         int widthHalf = mBackgroundNormal.getWidth()/2;
253         int heightHalf = mBackgroundNormal.getActualHeight()/2;
254         float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf);
255         Animator animator;
256         if (reverse) {
257             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
258                     widthHalf, heightHalf, radius, 0);
259         } else {
260             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
261                     widthHalf, heightHalf, 0, radius);
262         }
263         mBackgroundNormal.setVisibility(View.VISIBLE);
264         Interpolator interpolator;
265         Interpolator alphaInterpolator;
266         if (!reverse) {
267             interpolator = mLinearOutSlowInInterpolator;
268             alphaInterpolator = mLinearOutSlowInInterpolator;
269         } else {
270             interpolator = ACTIVATE_INVERSE_INTERPOLATOR;
271             alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR;
272         }
273         animator.setInterpolator(interpolator);
274         animator.setDuration(ACTIVATE_ANIMATION_LENGTH);
275         if (reverse) {
276             mBackgroundNormal.setAlpha(1f);
277             animator.addListener(new AnimatorListenerAdapter() {
278                 @Override
279                 public void onAnimationEnd(Animator animation) {
280                     if (mDimmed) {
281                         mBackgroundNormal.setVisibility(View.INVISIBLE);
282                     }
283                 }
284             });
285             animator.start();
286         } else {
287             mBackgroundNormal.setAlpha(0.4f);
288             animator.start();
289         }
290         mBackgroundNormal.animate()
291                 .alpha(reverse ? 0f : 1f)
292                 .setInterpolator(alphaInterpolator)
293                 .setDuration(ACTIVATE_ANIMATION_LENGTH);
294     }
295 
296     /**
297      * Cancels the hotspot and makes the notification inactive.
298      */
makeInactive(boolean animate)299     public void makeInactive(boolean animate) {
300         if (mActivated) {
301             if (mDimmed) {
302                 if (animate) {
303                     startActivateAnimation(true /* reverse */);
304                 } else {
305                     mBackgroundNormal.setVisibility(View.INVISIBLE);
306                 }
307             }
308             mActivated = false;
309         }
310         if (mOnActivatedListener != null) {
311             mOnActivatedListener.onActivationReset(this);
312         }
313         removeCallbacks(mTapTimeoutRunnable);
314     }
315 
isWithinTouchSlop(MotionEvent event)316     private boolean isWithinTouchSlop(MotionEvent event) {
317         return Math.abs(event.getX() - mDownX) < mTouchSlop
318                 && Math.abs(event.getY() - mDownY) < mTouchSlop;
319     }
320 
setDimmed(boolean dimmed, boolean fade)321     public void setDimmed(boolean dimmed, boolean fade) {
322         if (mDimmed != dimmed) {
323             mDimmed = dimmed;
324             if (fade) {
325                 fadeDimmedBackground();
326             } else {
327                 updateBackground();
328             }
329         }
330     }
331 
setDark(boolean dark, boolean fade, long delay)332     public void setDark(boolean dark, boolean fade, long delay) {
333         super.setDark(dark, fade, delay);
334         if (mDark == dark) {
335             return;
336         }
337         mDark = dark;
338         if (!dark && fade) {
339             if (mActivated) {
340                 mBackgroundDimmed.setVisibility(View.VISIBLE);
341                 mBackgroundNormal.setVisibility(View.VISIBLE);
342             } else if (mDimmed) {
343                 mBackgroundDimmed.setVisibility(View.VISIBLE);
344                 mBackgroundNormal.setVisibility(View.INVISIBLE);
345             } else {
346                 mBackgroundDimmed.setVisibility(View.INVISIBLE);
347                 mBackgroundNormal.setVisibility(View.VISIBLE);
348             }
349             fadeInFromDark(delay);
350         } else {
351             updateBackground();
352         }
353      }
354 
setShowingLegacyBackground(boolean showing)355     public void setShowingLegacyBackground(boolean showing) {
356         mShowingLegacyBackground = showing;
357         updateBackgroundTint();
358     }
359 
360     @Override
setBelowSpeedBump(boolean below)361     public void setBelowSpeedBump(boolean below) {
362         super.setBelowSpeedBump(below);
363         if (below != mIsBelowSpeedBump) {
364             mIsBelowSpeedBump = below;
365             updateBackgroundTint();
366         }
367     }
368 
369     /**
370      * Sets the tint color of the background
371      */
setTintColor(int color)372     public void setTintColor(int color) {
373         mBgTint = color;
374         updateBackgroundTint();
375     }
376 
updateBackgroundTint()377     private void updateBackgroundTint() {
378         int color = getBgColor();
379         int rippleColor = getRippleColor();
380         if (color == mNormalColor) {
381             // We don't need to tint a normal notification
382             color = 0;
383         }
384         mBackgroundDimmed.setTint(color);
385         mBackgroundNormal.setTint(color);
386         mBackgroundDimmed.setRippleColor(rippleColor);
387         mBackgroundNormal.setRippleColor(rippleColor);
388     }
389 
390     /**
391      * Fades in the background when exiting dark mode.
392      */
fadeInFromDark(long delay)393     private void fadeInFromDark(long delay) {
394         final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal;
395         background.setAlpha(0f);
396         background.setPivotX(mBackgroundDimmed.getWidth() / 2f);
397         background.setPivotY(getActualHeight() / 2f);
398         background.setScaleX(DARK_EXIT_SCALE_START);
399         background.setScaleY(DARK_EXIT_SCALE_START);
400         background.animate()
401                 .alpha(1f)
402                 .scaleX(1f)
403                 .scaleY(1f)
404                 .setDuration(DARK_ANIMATION_LENGTH)
405                 .setStartDelay(delay)
406                 .setInterpolator(mLinearOutSlowInInterpolator)
407                 .setListener(new AnimatorListenerAdapter() {
408                     @Override
409                     public void onAnimationCancel(Animator animation) {
410                         // Jump state if we are cancelled
411                         background.setScaleX(1f);
412                         background.setScaleY(1f);
413                         background.setAlpha(1f);
414                     }
415                 })
416                 .start();
417     }
418 
419     /**
420      * Fades the background when the dimmed state changes.
421      */
fadeDimmedBackground()422     private void fadeDimmedBackground() {
423         mBackgroundDimmed.animate().cancel();
424         mBackgroundNormal.animate().cancel();
425         if (mDimmed) {
426             mBackgroundDimmed.setVisibility(View.VISIBLE);
427         } else {
428             mBackgroundNormal.setVisibility(View.VISIBLE);
429         }
430         float startAlpha = mDimmed ? 1f : 0;
431         float endAlpha = mDimmed ? 0 : 1f;
432         int duration = BACKGROUND_ANIMATION_LENGTH_MS;
433         // Check whether there is already a background animation running.
434         if (mBackgroundAnimator != null) {
435             startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
436             duration = (int) mBackgroundAnimator.getCurrentPlayTime();
437             mBackgroundAnimator.removeAllListeners();
438             mBackgroundAnimator.cancel();
439             if (duration <= 0) {
440                 updateBackground();
441                 return;
442             }
443         }
444         mBackgroundNormal.setAlpha(startAlpha);
445         mBackgroundAnimator =
446                 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
447         mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator);
448         mBackgroundAnimator.setDuration(duration);
449         mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
450             @Override
451             public void onAnimationEnd(Animator animation) {
452                 if (mDimmed) {
453                     mBackgroundNormal.setVisibility(View.INVISIBLE);
454                 } else {
455                     mBackgroundDimmed.setVisibility(View.INVISIBLE);
456                 }
457                 mBackgroundAnimator = null;
458             }
459         });
460         mBackgroundAnimator.start();
461     }
462 
updateBackground()463     private void updateBackground() {
464         cancelFadeAnimations();
465         if (mDark) {
466             mBackgroundDimmed.setVisibility(View.INVISIBLE);
467             mBackgroundNormal.setVisibility(View.INVISIBLE);
468         } else if (mDimmed) {
469             mBackgroundDimmed.setVisibility(View.VISIBLE);
470             mBackgroundNormal.setVisibility(View.INVISIBLE);
471         } else {
472             mBackgroundDimmed.setVisibility(View.INVISIBLE);
473             mBackgroundNormal.setVisibility(View.VISIBLE);
474             mBackgroundNormal.setAlpha(1f);
475             removeCallbacks(mTapTimeoutRunnable);
476         }
477     }
478 
cancelFadeAnimations()479     private void cancelFadeAnimations() {
480         if (mBackgroundAnimator != null) {
481             mBackgroundAnimator.cancel();
482         }
483         mBackgroundDimmed.animate().cancel();
484         mBackgroundNormal.animate().cancel();
485     }
486 
487     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)488     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
489         super.onLayout(changed, left, top, right, bottom);
490         setPivotX(getWidth() / 2);
491     }
492 
493     @Override
setActualHeight(int actualHeight, boolean notifyListeners)494     public void setActualHeight(int actualHeight, boolean notifyListeners) {
495         super.setActualHeight(actualHeight, notifyListeners);
496         setPivotY(actualHeight / 2);
497         mBackgroundNormal.setActualHeight(actualHeight);
498         mBackgroundDimmed.setActualHeight(actualHeight);
499     }
500 
501     @Override
setClipTopAmount(int clipTopAmount)502     public void setClipTopAmount(int clipTopAmount) {
503         super.setClipTopAmount(clipTopAmount);
504         mBackgroundNormal.setClipTopAmount(clipTopAmount);
505         mBackgroundDimmed.setClipTopAmount(clipTopAmount);
506     }
507 
508     @Override
performRemoveAnimation(long duration, float translationDirection, Runnable onFinishedRunnable)509     public void performRemoveAnimation(long duration, float translationDirection,
510             Runnable onFinishedRunnable) {
511         enableAppearDrawing(true);
512         if (mDrawingAppearAnimation) {
513             startAppearAnimation(false /* isAppearing */, translationDirection,
514                     0, duration, onFinishedRunnable);
515         } else if (onFinishedRunnable != null) {
516             onFinishedRunnable.run();
517         }
518     }
519 
520     @Override
performAddAnimation(long delay, long duration)521     public void performAddAnimation(long delay, long duration) {
522         enableAppearDrawing(true);
523         if (mDrawingAppearAnimation) {
524             startAppearAnimation(true /* isAppearing */, -1.0f, delay, duration, null);
525         }
526     }
527 
startAppearAnimation(boolean isAppearing, float translationDirection, long delay, long duration, final Runnable onFinishedRunnable)528     private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay,
529             long duration, final Runnable onFinishedRunnable) {
530         cancelAppearAnimation();
531         mAnimationTranslationY = translationDirection * getActualHeight();
532         if (mAppearAnimationFraction == -1.0f) {
533             // not initialized yet, we start anew
534             if (isAppearing) {
535                 mAppearAnimationFraction = 0.0f;
536                 mAppearAnimationTranslation = mAnimationTranslationY;
537             } else {
538                 mAppearAnimationFraction = 1.0f;
539                 mAppearAnimationTranslation = 0;
540             }
541         }
542 
543         float targetValue;
544         if (isAppearing) {
545             mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
546             mCurrentAlphaInterpolator = mLinearOutSlowInInterpolator;
547             targetValue = 1.0f;
548         } else {
549             mCurrentAppearInterpolator = mFastOutSlowInInterpolator;
550             mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
551             targetValue = 0.0f;
552         }
553         mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
554                 targetValue);
555         mAppearAnimator.setInterpolator(mLinearInterpolator);
556         mAppearAnimator.setDuration(
557                 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
558         mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
559             @Override
560             public void onAnimationUpdate(ValueAnimator animation) {
561                 mAppearAnimationFraction = (float) animation.getAnimatedValue();
562                 updateAppearAnimationAlpha();
563                 updateAppearRect();
564                 invalidate();
565             }
566         });
567         if (delay > 0) {
568             // we need to apply the initial state already to avoid drawn frames in the wrong state
569             updateAppearAnimationAlpha();
570             updateAppearRect();
571             mAppearAnimator.setStartDelay(delay);
572         }
573         mAppearAnimator.addListener(new AnimatorListenerAdapter() {
574             private boolean mWasCancelled;
575 
576             @Override
577             public void onAnimationEnd(Animator animation) {
578                 if (onFinishedRunnable != null) {
579                     onFinishedRunnable.run();
580                 }
581                 if (!mWasCancelled) {
582                     mAppearAnimationFraction = -1;
583                     setOutlineRect(null);
584                     enableAppearDrawing(false);
585                 }
586             }
587 
588             @Override
589             public void onAnimationStart(Animator animation) {
590                 mWasCancelled = false;
591             }
592 
593             @Override
594             public void onAnimationCancel(Animator animation) {
595                 mWasCancelled = true;
596             }
597         });
598         mAppearAnimator.start();
599     }
600 
cancelAppearAnimation()601     private void cancelAppearAnimation() {
602         if (mAppearAnimator != null) {
603             mAppearAnimator.cancel();
604         }
605     }
606 
cancelAppearDrawing()607     public void cancelAppearDrawing() {
608         cancelAppearAnimation();
609         enableAppearDrawing(false);
610     }
611 
updateAppearRect()612     private void updateAppearRect() {
613         float inverseFraction = (1.0f - mAppearAnimationFraction);
614         float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
615         float translateYTotalAmount = translationFraction * mAnimationTranslationY;
616         mAppearAnimationTranslation = translateYTotalAmount;
617 
618         // handle width animation
619         float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
620                 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
621         widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
622         widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
623         float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) *
624                 widthFraction);
625         float right = getWidth() - left;
626 
627         // handle top animation
628         float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
629                 VERTICAL_ANIMATION_START;
630         heightFraction = Math.max(0.0f, heightFraction);
631         heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
632 
633         float top;
634         float bottom;
635         final int actualHeight = getActualHeight();
636         if (mAnimationTranslationY > 0.0f) {
637             bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f
638                     - translateYTotalAmount;
639             top = bottom * heightFraction;
640         } else {
641             top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f -
642                     translateYTotalAmount;
643             bottom = actualHeight * (1 - heightFraction) + top * heightFraction;
644         }
645         mAppearAnimationRect.set(left, top, right, bottom);
646         setOutlineRect(left, top + mAppearAnimationTranslation, right,
647                 bottom + mAppearAnimationTranslation);
648     }
649 
updateAppearAnimationAlpha()650     private void updateAppearAnimationAlpha() {
651         float contentAlphaProgress = mAppearAnimationFraction;
652         contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
653         contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
654         contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
655         setContentAlpha(contentAlphaProgress);
656     }
657 
setContentAlpha(float contentAlpha)658     private void setContentAlpha(float contentAlpha) {
659         View contentView = getContentView();
660         if (contentView.hasOverlappingRendering()) {
661             int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE
662                     : LAYER_TYPE_HARDWARE;
663             int currentLayerType = contentView.getLayerType();
664             if (currentLayerType != layerType) {
665                 contentView.setLayerType(layerType, null);
666             }
667         }
668         contentView.setAlpha(contentAlpha);
669     }
670 
getContentView()671     protected abstract View getContentView();
672 
getBgColor()673     private int getBgColor() {
674         if (mBgTint != 0) {
675             return mBgTint;
676         } else if (mShowingLegacyBackground) {
677             return mLegacyColor;
678         } else if (mIsBelowSpeedBump) {
679             return mLowPriorityColor;
680         } else {
681             return mNormalColor;
682         }
683     }
684 
getRippleColor()685     protected int getRippleColor() {
686         if (mBgTint != 0) {
687             return mTintedRippleColor;
688         } else if (mShowingLegacyBackground) {
689             return mTintedRippleColor;
690         } else if (mIsBelowSpeedBump) {
691             return mLowPriorityRippleColor;
692         } else {
693             return mNormalRippleColor;
694         }
695     }
696 
697     /**
698      * When we draw the appear animation, we render the view in a bitmap and render this bitmap
699      * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
700      * such that the normal drawing of the views does not happen anymore.
701      *
702      * @param enable Should it be enabled.
703      */
enableAppearDrawing(boolean enable)704     private void enableAppearDrawing(boolean enable) {
705         if (enable != mDrawingAppearAnimation) {
706             mDrawingAppearAnimation = enable;
707             if (!enable) {
708                 setContentAlpha(1.0f);
709             }
710             invalidate();
711         }
712     }
713 
714     @Override
dispatchDraw(Canvas canvas)715     protected void dispatchDraw(Canvas canvas) {
716         if (mDrawingAppearAnimation) {
717             canvas.save();
718             canvas.translate(0, mAppearAnimationTranslation);
719         }
720         super.dispatchDraw(canvas);
721         if (mDrawingAppearAnimation) {
722             canvas.restore();
723         }
724     }
725 
setOnActivatedListener(OnActivatedListener onActivatedListener)726     public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
727         mOnActivatedListener = onActivatedListener;
728     }
729 
reset()730     public void reset() {
731         setTintColor(0);
732         setShowingLegacyBackground(false);
733         setBelowSpeedBump(false);
734     }
735 
736     public interface OnActivatedListener {
onActivated(ActivatableNotificationView view)737         void onActivated(ActivatableNotificationView view);
onActivationReset(ActivatableNotificationView view)738         void onActivationReset(ActivatableNotificationView view);
739     }
740 }
741