• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         setOutlineAlpha(dark ? 0f : 1f);
354      }
355 
setShowingLegacyBackground(boolean showing)356     public void setShowingLegacyBackground(boolean showing) {
357         mShowingLegacyBackground = showing;
358         updateBackgroundTint();
359     }
360 
361     @Override
setBelowSpeedBump(boolean below)362     public void setBelowSpeedBump(boolean below) {
363         super.setBelowSpeedBump(below);
364         if (below != mIsBelowSpeedBump) {
365             mIsBelowSpeedBump = below;
366             updateBackgroundTint();
367         }
368     }
369 
370     /**
371      * Sets the tint color of the background
372      */
setTintColor(int color)373     public void setTintColor(int color) {
374         mBgTint = color;
375         updateBackgroundTint();
376     }
377 
updateBackgroundTint()378     private void updateBackgroundTint() {
379         int color = getBgColor();
380         int rippleColor = getRippleColor();
381         if (color == mNormalColor) {
382             // We don't need to tint a normal notification
383             color = 0;
384         }
385         mBackgroundDimmed.setTint(color);
386         mBackgroundNormal.setTint(color);
387         mBackgroundDimmed.setRippleColor(rippleColor);
388         mBackgroundNormal.setRippleColor(rippleColor);
389     }
390 
391     /**
392      * Fades in the background when exiting dark mode.
393      */
fadeInFromDark(long delay)394     private void fadeInFromDark(long delay) {
395         final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal;
396         background.setAlpha(0f);
397         background.setPivotX(mBackgroundDimmed.getWidth() / 2f);
398         background.setPivotY(getActualHeight() / 2f);
399         background.setScaleX(DARK_EXIT_SCALE_START);
400         background.setScaleY(DARK_EXIT_SCALE_START);
401         background.animate()
402                 .alpha(1f)
403                 .scaleX(1f)
404                 .scaleY(1f)
405                 .setDuration(DARK_ANIMATION_LENGTH)
406                 .setStartDelay(delay)
407                 .setInterpolator(mLinearOutSlowInInterpolator)
408                 .setListener(new AnimatorListenerAdapter() {
409                     @Override
410                     public void onAnimationCancel(Animator animation) {
411                         // Jump state if we are cancelled
412                         background.setScaleX(1f);
413                         background.setScaleY(1f);
414                         background.setAlpha(1f);
415                     }
416                 })
417                 .start();
418     }
419 
420     /**
421      * Fades the background when the dimmed state changes.
422      */
fadeDimmedBackground()423     private void fadeDimmedBackground() {
424         mBackgroundDimmed.animate().cancel();
425         mBackgroundNormal.animate().cancel();
426         if (mDimmed) {
427             mBackgroundDimmed.setVisibility(View.VISIBLE);
428         } else {
429             mBackgroundNormal.setVisibility(View.VISIBLE);
430         }
431         float startAlpha = mDimmed ? 1f : 0;
432         float endAlpha = mDimmed ? 0 : 1f;
433         int duration = BACKGROUND_ANIMATION_LENGTH_MS;
434         // Check whether there is already a background animation running.
435         if (mBackgroundAnimator != null) {
436             startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
437             duration = (int) mBackgroundAnimator.getCurrentPlayTime();
438             mBackgroundAnimator.removeAllListeners();
439             mBackgroundAnimator.cancel();
440             if (duration <= 0) {
441                 updateBackground();
442                 return;
443             }
444         }
445         mBackgroundNormal.setAlpha(startAlpha);
446         mBackgroundAnimator =
447                 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
448         mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator);
449         mBackgroundAnimator.setDuration(duration);
450         mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
451             @Override
452             public void onAnimationEnd(Animator animation) {
453                 if (mDimmed) {
454                     mBackgroundNormal.setVisibility(View.INVISIBLE);
455                 } else {
456                     mBackgroundDimmed.setVisibility(View.INVISIBLE);
457                 }
458                 mBackgroundAnimator = null;
459             }
460         });
461         mBackgroundAnimator.start();
462     }
463 
updateBackground()464     private void updateBackground() {
465         cancelFadeAnimations();
466         if (mDark) {
467             mBackgroundDimmed.setVisibility(View.INVISIBLE);
468             mBackgroundNormal.setVisibility(View.INVISIBLE);
469         } else if (mDimmed) {
470             mBackgroundDimmed.setVisibility(View.VISIBLE);
471             mBackgroundNormal.setVisibility(View.INVISIBLE);
472         } else {
473             mBackgroundDimmed.setVisibility(View.INVISIBLE);
474             mBackgroundNormal.setVisibility(View.VISIBLE);
475             mBackgroundNormal.setAlpha(1f);
476             removeCallbacks(mTapTimeoutRunnable);
477         }
478     }
479 
cancelFadeAnimations()480     private void cancelFadeAnimations() {
481         if (mBackgroundAnimator != null) {
482             mBackgroundAnimator.cancel();
483         }
484         mBackgroundDimmed.animate().cancel();
485         mBackgroundNormal.animate().cancel();
486     }
487 
488     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)489     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
490         super.onLayout(changed, left, top, right, bottom);
491         setPivotX(getWidth() / 2);
492     }
493 
494     @Override
setActualHeight(int actualHeight, boolean notifyListeners)495     public void setActualHeight(int actualHeight, boolean notifyListeners) {
496         super.setActualHeight(actualHeight, notifyListeners);
497         setPivotY(actualHeight / 2);
498         mBackgroundNormal.setActualHeight(actualHeight);
499         mBackgroundDimmed.setActualHeight(actualHeight);
500     }
501 
502     @Override
setClipTopAmount(int clipTopAmount)503     public void setClipTopAmount(int clipTopAmount) {
504         super.setClipTopAmount(clipTopAmount);
505         mBackgroundNormal.setClipTopAmount(clipTopAmount);
506         mBackgroundDimmed.setClipTopAmount(clipTopAmount);
507     }
508 
509     @Override
performRemoveAnimation(long duration, float translationDirection, Runnable onFinishedRunnable)510     public void performRemoveAnimation(long duration, float translationDirection,
511             Runnable onFinishedRunnable) {
512         enableAppearDrawing(true);
513         if (mDrawingAppearAnimation) {
514             startAppearAnimation(false /* isAppearing */, translationDirection,
515                     0, duration, onFinishedRunnable);
516         } else if (onFinishedRunnable != null) {
517             onFinishedRunnable.run();
518         }
519     }
520 
521     @Override
performAddAnimation(long delay, long duration)522     public void performAddAnimation(long delay, long duration) {
523         enableAppearDrawing(true);
524         if (mDrawingAppearAnimation) {
525             startAppearAnimation(true /* isAppearing */, -1.0f, delay, duration, null);
526         }
527     }
528 
startAppearAnimation(boolean isAppearing, float translationDirection, long delay, long duration, final Runnable onFinishedRunnable)529     private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay,
530             long duration, final Runnable onFinishedRunnable) {
531         cancelAppearAnimation();
532         mAnimationTranslationY = translationDirection * getActualHeight();
533         if (mAppearAnimationFraction == -1.0f) {
534             // not initialized yet, we start anew
535             if (isAppearing) {
536                 mAppearAnimationFraction = 0.0f;
537                 mAppearAnimationTranslation = mAnimationTranslationY;
538             } else {
539                 mAppearAnimationFraction = 1.0f;
540                 mAppearAnimationTranslation = 0;
541             }
542         }
543 
544         float targetValue;
545         if (isAppearing) {
546             mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
547             mCurrentAlphaInterpolator = mLinearOutSlowInInterpolator;
548             targetValue = 1.0f;
549         } else {
550             mCurrentAppearInterpolator = mFastOutSlowInInterpolator;
551             mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
552             targetValue = 0.0f;
553         }
554         mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
555                 targetValue);
556         mAppearAnimator.setInterpolator(mLinearInterpolator);
557         mAppearAnimator.setDuration(
558                 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
559         mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
560             @Override
561             public void onAnimationUpdate(ValueAnimator animation) {
562                 mAppearAnimationFraction = (float) animation.getAnimatedValue();
563                 updateAppearAnimationAlpha();
564                 updateAppearRect();
565                 invalidate();
566             }
567         });
568         if (delay > 0) {
569             // we need to apply the initial state already to avoid drawn frames in the wrong state
570             updateAppearAnimationAlpha();
571             updateAppearRect();
572             mAppearAnimator.setStartDelay(delay);
573         }
574         mAppearAnimator.addListener(new AnimatorListenerAdapter() {
575             private boolean mWasCancelled;
576 
577             @Override
578             public void onAnimationEnd(Animator animation) {
579                 if (onFinishedRunnable != null) {
580                     onFinishedRunnable.run();
581                 }
582                 if (!mWasCancelled) {
583                     mAppearAnimationFraction = -1;
584                     setOutlineRect(null);
585                     enableAppearDrawing(false);
586                 }
587             }
588 
589             @Override
590             public void onAnimationStart(Animator animation) {
591                 mWasCancelled = false;
592             }
593 
594             @Override
595             public void onAnimationCancel(Animator animation) {
596                 mWasCancelled = true;
597             }
598         });
599         mAppearAnimator.start();
600     }
601 
cancelAppearAnimation()602     private void cancelAppearAnimation() {
603         if (mAppearAnimator != null) {
604             mAppearAnimator.cancel();
605         }
606     }
607 
cancelAppearDrawing()608     public void cancelAppearDrawing() {
609         cancelAppearAnimation();
610         enableAppearDrawing(false);
611     }
612 
updateAppearRect()613     private void updateAppearRect() {
614         float inverseFraction = (1.0f - mAppearAnimationFraction);
615         float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
616         float translateYTotalAmount = translationFraction * mAnimationTranslationY;
617         mAppearAnimationTranslation = translateYTotalAmount;
618 
619         // handle width animation
620         float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
621                 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
622         widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
623         widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
624         float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) *
625                 widthFraction);
626         float right = getWidth() - left;
627 
628         // handle top animation
629         float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
630                 VERTICAL_ANIMATION_START;
631         heightFraction = Math.max(0.0f, heightFraction);
632         heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
633 
634         float top;
635         float bottom;
636         final int actualHeight = getActualHeight();
637         if (mAnimationTranslationY > 0.0f) {
638             bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f
639                     - translateYTotalAmount;
640             top = bottom * heightFraction;
641         } else {
642             top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f -
643                     translateYTotalAmount;
644             bottom = actualHeight * (1 - heightFraction) + top * heightFraction;
645         }
646         mAppearAnimationRect.set(left, top, right, bottom);
647         setOutlineRect(left, top + mAppearAnimationTranslation, right,
648                 bottom + mAppearAnimationTranslation);
649     }
650 
updateAppearAnimationAlpha()651     private void updateAppearAnimationAlpha() {
652         float contentAlphaProgress = mAppearAnimationFraction;
653         contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
654         contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
655         contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
656         setContentAlpha(contentAlphaProgress);
657     }
658 
setContentAlpha(float contentAlpha)659     private void setContentAlpha(float contentAlpha) {
660         View contentView = getContentView();
661         if (contentView.hasOverlappingRendering()) {
662             int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE
663                     : LAYER_TYPE_HARDWARE;
664             int currentLayerType = contentView.getLayerType();
665             if (currentLayerType != layerType) {
666                 contentView.setLayerType(layerType, null);
667             }
668         }
669         contentView.setAlpha(contentAlpha);
670     }
671 
getContentView()672     protected abstract View getContentView();
673 
getBgColor()674     private int getBgColor() {
675         if (mBgTint != 0) {
676             return mBgTint;
677         } else if (mShowingLegacyBackground) {
678             return mLegacyColor;
679         } else if (mIsBelowSpeedBump) {
680             return mLowPriorityColor;
681         } else {
682             return mNormalColor;
683         }
684     }
685 
getRippleColor()686     protected int getRippleColor() {
687         if (mBgTint != 0) {
688             return mTintedRippleColor;
689         } else if (mShowingLegacyBackground) {
690             return mTintedRippleColor;
691         } else if (mIsBelowSpeedBump) {
692             return mLowPriorityRippleColor;
693         } else {
694             return mNormalRippleColor;
695         }
696     }
697 
698     /**
699      * When we draw the appear animation, we render the view in a bitmap and render this bitmap
700      * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
701      * such that the normal drawing of the views does not happen anymore.
702      *
703      * @param enable Should it be enabled.
704      */
enableAppearDrawing(boolean enable)705     private void enableAppearDrawing(boolean enable) {
706         if (enable != mDrawingAppearAnimation) {
707             mDrawingAppearAnimation = enable;
708             if (!enable) {
709                 setContentAlpha(1.0f);
710             }
711             invalidate();
712         }
713     }
714 
715     @Override
dispatchDraw(Canvas canvas)716     protected void dispatchDraw(Canvas canvas) {
717         if (mDrawingAppearAnimation) {
718             canvas.save();
719             canvas.translate(0, mAppearAnimationTranslation);
720         }
721         super.dispatchDraw(canvas);
722         if (mDrawingAppearAnimation) {
723             canvas.restore();
724         }
725     }
726 
setOnActivatedListener(OnActivatedListener onActivatedListener)727     public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
728         mOnActivatedListener = onActivatedListener;
729     }
730 
reset()731     public void reset() {
732         setTintColor(0);
733         setShowingLegacyBackground(false);
734         setBelowSpeedBump(false);
735     }
736 
737     public interface OnActivatedListener {
onActivated(ActivatableNotificationView view)738         void onActivated(ActivatableNotificationView view);
onActivationReset(ActivatableNotificationView view)739         void onActivationReset(ActivatableNotificationView view);
740     }
741 }
742