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