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