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.notification.row;
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.Color;
26 import android.graphics.RectF;
27 import android.util.AttributeSet;
28 import android.util.MathUtils;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.ViewAnimationUtils;
32 import android.view.accessibility.AccessibilityManager;
33 import android.view.animation.Interpolator;
34 import android.view.animation.PathInterpolator;
35 
36 import com.android.systemui.Dependency;
37 import com.android.systemui.Interpolators;
38 import com.android.systemui.R;
39 import com.android.systemui.plugins.FalsingManager;
40 import com.android.systemui.statusbar.NotificationShelf;
41 import com.android.systemui.statusbar.notification.FakeShadowView;
42 import com.android.systemui.statusbar.notification.NotificationUtils;
43 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
44 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
45 import com.android.systemui.statusbar.phone.DoubleTapHelper;
46 
47 /**
48  * Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf}
49  * to implement dimming/activating on Keyguard for the double-tap gesture
50  */
51 public abstract class ActivatableNotificationView extends ExpandableOutlineView {
52 
53     private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
54     private static final int ACTIVATE_ANIMATION_LENGTH = 220;
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      * A sentinel value when no color should be used. Can be used with {@link #setTintColor(int)}
88      * or {@link #setOverrideTintColor(int, float)}.
89      */
90     protected static final int NO_COLOR = 0;
91 
92     private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
93             = new PathInterpolator(0.6f, 0, 0.5f, 1);
94     private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
95             = new PathInterpolator(0, 0, 0.5f, 1);
96     private int mTintedRippleColor;
97     protected int mNormalRippleColor;
98     private final AccessibilityManager mAccessibilityManager;
99     private final DoubleTapHelper mDoubleTapHelper;
100 
101     private boolean mDimmed;
102 
103     protected int mBgTint = NO_COLOR;
104     private float mBgAlpha = 1f;
105 
106     /**
107      * Flag to indicate that the notification has been touched once and the second touch will
108      * click it.
109      */
110     private boolean mActivated;
111 
112     private OnActivatedListener mOnActivatedListener;
113 
114     private final Interpolator mSlowOutFastInInterpolator;
115     private final Interpolator mSlowOutLinearInInterpolator;
116     private Interpolator mCurrentAppearInterpolator;
117     private Interpolator mCurrentAlphaInterpolator;
118 
119     protected NotificationBackgroundView mBackgroundNormal;
120     private NotificationBackgroundView mBackgroundDimmed;
121     private ObjectAnimator mBackgroundAnimator;
122     private RectF mAppearAnimationRect = new RectF();
123     private float mAnimationTranslationY;
124     private boolean mDrawingAppearAnimation;
125     private ValueAnimator mAppearAnimator;
126     private ValueAnimator mBackgroundColorAnimator;
127     private float mAppearAnimationFraction = -1.0f;
128     private float mAppearAnimationTranslation;
129     private int mNormalColor;
130     private boolean mLastInSection;
131     private boolean mFirstInSection;
132     private boolean mIsBelowSpeedBump;
133     private final FalsingManager mFalsingManager;
134 
135     private float mNormalBackgroundVisibilityAmount;
136     private float mDimmedBackgroundFadeInAmount = -1;
137     private ValueAnimator.AnimatorUpdateListener mBackgroundVisibilityUpdater
138             = new ValueAnimator.AnimatorUpdateListener() {
139         @Override
140         public void onAnimationUpdate(ValueAnimator animation) {
141             setNormalBackgroundVisibilityAmount(mBackgroundNormal.getAlpha());
142             mDimmedBackgroundFadeInAmount = mBackgroundDimmed.getAlpha();
143         }
144     };
145     private FakeShadowView mFakeShadow;
146     private int mCurrentBackgroundTint;
147     private int mTargetTint;
148     private int mStartTint;
149     private int mOverrideTint;
150     private float mOverrideAmount;
151     private boolean mShadowHidden;
152     /**
153      * Similar to mDimmed but is also true if it's not dimmable but should be
154      */
155     private boolean mNeedsDimming;
156     private int mDimmedAlpha;
157     private boolean mBlockNextTouch;
158     private boolean mIsHeadsUpAnimation;
159     private int mHeadsUpAddStartLocation;
160     private float mHeadsUpLocation;
161     private boolean mIsAppearing;
162 
ActivatableNotificationView(Context context, AttributeSet attrs)163     public ActivatableNotificationView(Context context, AttributeSet attrs) {
164         super(context, attrs);
165         mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
166         mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
167         mFalsingManager = Dependency.get(FalsingManager.class);  // TODO: inject into a controller.
168         setClipChildren(false);
169         setClipToPadding(false);
170         updateColors();
171         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
172 
173         mDoubleTapHelper = new DoubleTapHelper(this, (active) -> {
174             if (active) {
175                 makeActive();
176             } else {
177                 makeInactive(true /* animate */);
178             }
179         }, super::performClick, this::handleSlideBack, mFalsingManager::onNotificationDoubleTap);
180         initDimens();
181     }
182 
updateColors()183     private void updateColors() {
184         mNormalColor = mContext.getColor(R.color.notification_material_background_color);
185         mTintedRippleColor = mContext.getColor(
186                 R.color.notification_ripple_tinted_color);
187         mNormalRippleColor = mContext.getColor(
188                 R.color.notification_ripple_untinted_color);
189         mDimmedAlpha = Color.alpha(mContext.getColor(
190                 R.color.notification_material_background_dimmed_color));
191     }
192 
initDimens()193     private void initDimens() {
194         mHeadsUpAddStartLocation = getResources().getDimensionPixelSize(
195                 com.android.internal.R.dimen.notification_content_margin_start);
196     }
197 
198     @Override
onDensityOrFontScaleChanged()199     public void onDensityOrFontScaleChanged() {
200         super.onDensityOrFontScaleChanged();
201         initDimens();
202     }
203 
updateBackgroundColors()204     protected void updateBackgroundColors() {
205         updateColors();
206         initBackground();
207         updateBackgroundTint();
208     }
209 
210     @Override
onFinishInflate()211     protected void onFinishInflate() {
212         super.onFinishInflate();
213         mBackgroundNormal = findViewById(R.id.backgroundNormal);
214         mFakeShadow = findViewById(R.id.fake_shadow);
215         mShadowHidden = mFakeShadow.getVisibility() != VISIBLE;
216         mBackgroundDimmed = findViewById(R.id.backgroundDimmed);
217         initBackground();
218         updateBackground();
219         updateBackgroundTint();
220         updateOutlineAlpha();
221     }
222 
223     /**
224      * Sets the custom backgrounds on {@link #mBackgroundNormal} and {@link #mBackgroundDimmed}.
225      * This method can also be used to reload the backgrounds on both of those views, which can
226      * be useful in a configuration change.
227      */
initBackground()228     protected void initBackground() {
229         mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg);
230         mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim);
231     }
232 
233     private final Runnable mTapTimeoutRunnable = new Runnable() {
234         @Override
235         public void run() {
236             makeInactive(true /* animate */);
237         }
238     };
239 
240     @Override
onInterceptTouchEvent(MotionEvent ev)241     public boolean onInterceptTouchEvent(MotionEvent ev) {
242         if (mNeedsDimming && ev.getActionMasked() == MotionEvent.ACTION_DOWN
243                 && disallowSingleClick(ev) && !isTouchExplorationEnabled()) {
244             if (!mActivated) {
245                 return true;
246             } else if (!mDoubleTapHelper.isWithinDoubleTapSlop(ev)) {
247                 mBlockNextTouch = true;
248                 makeInactive(true /* animate */);
249                 return true;
250             }
251         }
252         return super.onInterceptTouchEvent(ev);
253     }
254 
isTouchExplorationEnabled()255     private boolean isTouchExplorationEnabled() {
256         return mAccessibilityManager.isTouchExplorationEnabled();
257     }
258 
disallowSingleClick(MotionEvent ev)259     protected boolean disallowSingleClick(MotionEvent ev) {
260         return false;
261     }
262 
handleSlideBack()263     protected boolean handleSlideBack() {
264         return false;
265     }
266 
267     @Override
onTouchEvent(MotionEvent event)268     public boolean onTouchEvent(MotionEvent event) {
269         boolean result;
270         if (mBlockNextTouch) {
271             mBlockNextTouch = false;
272             return false;
273         }
274         if (mNeedsDimming && !isTouchExplorationEnabled() && isInteractive()) {
275             boolean wasActivated = mActivated;
276             result = handleTouchEventDimmed(event);
277             if (wasActivated && result && event.getAction() == MotionEvent.ACTION_UP) {
278                 removeCallbacks(mTapTimeoutRunnable);
279             }
280         } else {
281             result = super.onTouchEvent(event);
282         }
283         return result;
284     }
285 
286     /**
287      * @return whether this view is interactive and can be double tapped
288      */
isInteractive()289     protected boolean isInteractive() {
290         return true;
291     }
292 
293     @Override
drawableHotspotChanged(float x, float y)294     public void drawableHotspotChanged(float x, float y) {
295         if (!mDimmed){
296             mBackgroundNormal.drawableHotspotChanged(x, y);
297         }
298     }
299 
300     @Override
drawableStateChanged()301     protected void drawableStateChanged() {
302         super.drawableStateChanged();
303         if (mDimmed) {
304             mBackgroundDimmed.setState(getDrawableState());
305         } else {
306             mBackgroundNormal.setState(getDrawableState());
307         }
308     }
309 
setRippleAllowed(boolean allowed)310     public void setRippleAllowed(boolean allowed) {
311         mBackgroundNormal.setPressedAllowed(allowed);
312     }
313 
handleTouchEventDimmed(MotionEvent event)314     private boolean handleTouchEventDimmed(MotionEvent event) {
315         if (mNeedsDimming && !mDimmed) {
316             // We're actually dimmed, but our content isn't dimmable, let's ensure we have a ripple
317             super.onTouchEvent(event);
318         }
319         return mDoubleTapHelper.onTouchEvent(event, getActualHeight());
320     }
321 
322     @Override
performClick()323     public boolean performClick() {
324         if (!mNeedsDimming || isTouchExplorationEnabled()) {
325             return super.performClick();
326         }
327         return false;
328     }
329 
makeActive()330     private void makeActive() {
331         mFalsingManager.onNotificationActive();
332         startActivateAnimation(false /* reverse */);
333         mActivated = true;
334         if (mOnActivatedListener != null) {
335             mOnActivatedListener.onActivated(this);
336         }
337     }
338 
startActivateAnimation(final boolean reverse)339     private void startActivateAnimation(final boolean reverse) {
340         if (!isAttachedToWindow()) {
341             return;
342         }
343         if (!isDimmable()) {
344             return;
345         }
346         int widthHalf = mBackgroundNormal.getWidth()/2;
347         int heightHalf = mBackgroundNormal.getActualHeight()/2;
348         float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf);
349         Animator animator;
350         if (reverse) {
351             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
352                     widthHalf, heightHalf, radius, 0);
353         } else {
354             animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
355                     widthHalf, heightHalf, 0, radius);
356         }
357         mBackgroundNormal.setVisibility(View.VISIBLE);
358         Interpolator interpolator;
359         Interpolator alphaInterpolator;
360         if (!reverse) {
361             interpolator = Interpolators.LINEAR_OUT_SLOW_IN;
362             alphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
363         } else {
364             interpolator = ACTIVATE_INVERSE_INTERPOLATOR;
365             alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR;
366         }
367         animator.setInterpolator(interpolator);
368         animator.setDuration(ACTIVATE_ANIMATION_LENGTH);
369         if (reverse) {
370             mBackgroundNormal.setAlpha(1f);
371             animator.addListener(new AnimatorListenerAdapter() {
372                 @Override
373                 public void onAnimationEnd(Animator animation) {
374                     updateBackground();
375                 }
376             });
377             animator.start();
378         } else {
379             mBackgroundNormal.setAlpha(0.4f);
380             animator.start();
381         }
382         mBackgroundNormal.animate()
383                 .alpha(reverse ? 0f : 1f)
384                 .setInterpolator(alphaInterpolator)
385                 .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
386                     @Override
387                     public void onAnimationUpdate(ValueAnimator animation) {
388                         float animatedFraction = animation.getAnimatedFraction();
389                         if (reverse) {
390                             animatedFraction = 1.0f - animatedFraction;
391                         }
392                         setNormalBackgroundVisibilityAmount(animatedFraction);
393                     }
394                 })
395                 .setDuration(ACTIVATE_ANIMATION_LENGTH);
396     }
397 
398     /**
399      * Cancels the hotspot and makes the notification inactive.
400      */
makeInactive(boolean animate)401     public void makeInactive(boolean animate) {
402         if (mActivated) {
403             mActivated = false;
404             if (mDimmed) {
405                 if (animate) {
406                     startActivateAnimation(true /* reverse */);
407                 } else {
408                     updateBackground();
409                 }
410             }
411         }
412         if (mOnActivatedListener != null) {
413             mOnActivatedListener.onActivationReset(this);
414         }
415         removeCallbacks(mTapTimeoutRunnable);
416     }
417 
setDimmed(boolean dimmed, boolean fade)418     public void setDimmed(boolean dimmed, boolean fade) {
419         mNeedsDimming = dimmed;
420         dimmed &= isDimmable();
421         if (mDimmed != dimmed) {
422             mDimmed = dimmed;
423             resetBackgroundAlpha();
424             if (fade) {
425                 fadeDimmedBackground();
426             } else {
427                 updateBackground();
428             }
429         }
430     }
431 
isDimmable()432     public boolean isDimmable() {
433         return true;
434     }
435 
updateOutlineAlpha()436     private void updateOutlineAlpha() {
437         float alpha = NotificationStackScrollLayout.BACKGROUND_ALPHA_DIMMED;
438         alpha = (alpha + (1.0f - alpha) * mNormalBackgroundVisibilityAmount);
439         setOutlineAlpha(alpha);
440     }
441 
setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount)442     public void setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount) {
443         mNormalBackgroundVisibilityAmount = normalBackgroundVisibilityAmount;
444         updateOutlineAlpha();
445     }
446 
447     @Override
setBelowSpeedBump(boolean below)448     public void setBelowSpeedBump(boolean below) {
449         super.setBelowSpeedBump(below);
450         if (below != mIsBelowSpeedBump) {
451             mIsBelowSpeedBump = below;
452             updateBackgroundTint();
453             onBelowSpeedBumpChanged();
454         }
455     }
456 
onBelowSpeedBumpChanged()457     protected void onBelowSpeedBumpChanged() {
458     }
459 
460     /**
461      * @return whether we are below the speed bump
462      */
isBelowSpeedBump()463     public boolean isBelowSpeedBump() {
464         return mIsBelowSpeedBump;
465     }
466 
467     /**
468      * Sets the tint color of the background
469      */
setTintColor(int color)470     public void setTintColor(int color) {
471         setTintColor(color, false);
472     }
473 
474     /**
475      * Sets the tint color of the background
476      */
setTintColor(int color, boolean animated)477     public void setTintColor(int color, boolean animated) {
478         if (color != mBgTint) {
479             mBgTint = color;
480             updateBackgroundTint(animated);
481         }
482     }
483 
484     @Override
setDistanceToTopRoundness(float distanceToTopRoundness)485     public void setDistanceToTopRoundness(float distanceToTopRoundness) {
486         super.setDistanceToTopRoundness(distanceToTopRoundness);
487         mBackgroundNormal.setDistanceToTopRoundness(distanceToTopRoundness);
488         mBackgroundDimmed.setDistanceToTopRoundness(distanceToTopRoundness);
489     }
490 
isLastInSection()491     public boolean isLastInSection() {
492         return mLastInSection;
493     }
494 
isFirstInSection()495     public boolean isFirstInSection() {
496         return mFirstInSection;
497     }
498 
499     /** Sets whether this view is the last notification in a section. */
setLastInSection(boolean lastInSection)500     public void setLastInSection(boolean lastInSection) {
501         if (lastInSection != mLastInSection) {
502             mLastInSection = lastInSection;
503             mBackgroundNormal.setLastInSection(lastInSection);
504             mBackgroundDimmed.setLastInSection(lastInSection);
505         }
506     }
507 
508     /** Sets whether this view is the first notification in a section. */
setFirstInSection(boolean firstInSection)509     public void setFirstInSection(boolean firstInSection) {
510         if (firstInSection != mFirstInSection) {
511             mFirstInSection = firstInSection;
512             mBackgroundNormal.setFirstInSection(firstInSection);
513             mBackgroundDimmed.setFirstInSection(firstInSection);
514         }
515     }
516 
517     /**
518      * Set an override tint color that is used for the background.
519      *
520      * @param color the color that should be used to tint the background.
521      *              This can be {@link #NO_COLOR} if the tint should be normally computed.
522      * @param overrideAmount a value from 0 to 1 how much the override tint should be used. The
523      *                       background color will then be the interpolation between this and the
524      *                       regular background color, where 1 means the overrideTintColor is fully
525      *                       used and the background color not at all.
526      */
setOverrideTintColor(int color, float overrideAmount)527     public void setOverrideTintColor(int color, float overrideAmount) {
528         mOverrideTint = color;
529         mOverrideAmount = overrideAmount;
530         int newColor = calculateBgColor();
531         setBackgroundTintColor(newColor);
532         if (!isDimmable() && mNeedsDimming) {
533            mBackgroundNormal.setDrawableAlpha((int) NotificationUtils.interpolate(255,
534                    mDimmedAlpha,
535                    overrideAmount));
536         } else {
537             mBackgroundNormal.setDrawableAlpha(255);
538         }
539     }
540 
updateBackgroundTint()541     protected void updateBackgroundTint() {
542         updateBackgroundTint(false /* animated */);
543     }
544 
updateBackgroundTint(boolean animated)545     private void updateBackgroundTint(boolean animated) {
546         if (mBackgroundColorAnimator != null) {
547             mBackgroundColorAnimator.cancel();
548         }
549         int rippleColor = getRippleColor();
550         mBackgroundDimmed.setRippleColor(rippleColor);
551         mBackgroundNormal.setRippleColor(rippleColor);
552         int color = calculateBgColor();
553         if (!animated) {
554             setBackgroundTintColor(color);
555         } else if (color != mCurrentBackgroundTint) {
556             mStartTint = mCurrentBackgroundTint;
557             mTargetTint = color;
558             mBackgroundColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
559             mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
560                 @Override
561                 public void onAnimationUpdate(ValueAnimator animation) {
562                     int newColor = NotificationUtils.interpolateColors(mStartTint, mTargetTint,
563                             animation.getAnimatedFraction());
564                     setBackgroundTintColor(newColor);
565                 }
566             });
567             mBackgroundColorAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
568             mBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR);
569             mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() {
570                 @Override
571                 public void onAnimationEnd(Animator animation) {
572                     mBackgroundColorAnimator = null;
573                 }
574             });
575             mBackgroundColorAnimator.start();
576         }
577     }
578 
setBackgroundTintColor(int color)579     protected void setBackgroundTintColor(int color) {
580         if (color != mCurrentBackgroundTint) {
581             mCurrentBackgroundTint = color;
582             if (color == mNormalColor) {
583                 // We don't need to tint a normal notification
584                 color = 0;
585             }
586             mBackgroundDimmed.setTint(color);
587             mBackgroundNormal.setTint(color);
588         }
589     }
590 
591     /**
592      * Fades the background when the dimmed state changes.
593      */
fadeDimmedBackground()594     private void fadeDimmedBackground() {
595         mBackgroundDimmed.animate().cancel();
596         mBackgroundNormal.animate().cancel();
597         if (mActivated) {
598             updateBackground();
599             return;
600         }
601         if (!shouldHideBackground()) {
602             if (mDimmed) {
603                 mBackgroundDimmed.setVisibility(View.VISIBLE);
604             } else {
605                 mBackgroundNormal.setVisibility(View.VISIBLE);
606             }
607         }
608         float startAlpha = mDimmed ? 1f : 0;
609         float endAlpha = mDimmed ? 0 : 1f;
610         int duration = BACKGROUND_ANIMATION_LENGTH_MS;
611         // Check whether there is already a background animation running.
612         if (mBackgroundAnimator != null) {
613             startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
614             duration = (int) mBackgroundAnimator.getCurrentPlayTime();
615             mBackgroundAnimator.removeAllListeners();
616             mBackgroundAnimator.cancel();
617             if (duration <= 0) {
618                 updateBackground();
619                 return;
620             }
621         }
622         mBackgroundNormal.setAlpha(startAlpha);
623         mBackgroundAnimator =
624                 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
625         mBackgroundAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
626         mBackgroundAnimator.setDuration(duration);
627         mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
628             @Override
629             public void onAnimationEnd(Animator animation) {
630                 updateBackground();
631                 mBackgroundAnimator = null;
632                 mDimmedBackgroundFadeInAmount = -1;
633             }
634         });
635         mBackgroundAnimator.addUpdateListener(mBackgroundVisibilityUpdater);
636         mBackgroundAnimator.start();
637     }
638 
updateBackgroundAlpha(float transformationAmount)639     protected void updateBackgroundAlpha(float transformationAmount) {
640         mBgAlpha =  isChildInGroup() && mDimmed ? transformationAmount : 1f;
641         if (mDimmedBackgroundFadeInAmount != -1) {
642             mBgAlpha *= mDimmedBackgroundFadeInAmount;
643         }
644         mBackgroundDimmed.setAlpha(mBgAlpha);
645     }
646 
resetBackgroundAlpha()647     protected void resetBackgroundAlpha() {
648         updateBackgroundAlpha(0f /* transformationAmount */);
649     }
650 
updateBackground()651     protected void updateBackground() {
652         cancelFadeAnimations();
653         if (shouldHideBackground()) {
654             mBackgroundDimmed.setVisibility(INVISIBLE);
655             mBackgroundNormal.setVisibility(mActivated ? VISIBLE : INVISIBLE);
656         } else if (mDimmed) {
657             // When groups are animating to the expanded state from the lockscreen, show the
658             // normal background instead of the dimmed background.
659             final boolean dontShowDimmed = isGroupExpansionChanging() && isChildInGroup();
660             mBackgroundDimmed.setVisibility(dontShowDimmed ? View.INVISIBLE : View.VISIBLE);
661             mBackgroundNormal.setVisibility((mActivated || dontShowDimmed)
662                     ? View.VISIBLE
663                     : View.INVISIBLE);
664         } else {
665             mBackgroundDimmed.setVisibility(View.INVISIBLE);
666             mBackgroundNormal.setVisibility(View.VISIBLE);
667             mBackgroundNormal.setAlpha(1f);
668             removeCallbacks(mTapTimeoutRunnable);
669             // make in inactive to avoid it sticking around active
670             makeInactive(false /* animate */);
671         }
672         setNormalBackgroundVisibilityAmount(
673                 mBackgroundNormal.getVisibility() == View.VISIBLE ? 1.0f : 0.0f);
674     }
675 
updateBackgroundClipping()676     protected void updateBackgroundClipping() {
677         mBackgroundNormal.setBottomAmountClips(!isChildInGroup());
678         mBackgroundDimmed.setBottomAmountClips(!isChildInGroup());
679     }
680 
shouldHideBackground()681     protected boolean shouldHideBackground() {
682         return false;
683     }
684 
cancelFadeAnimations()685     private void cancelFadeAnimations() {
686         if (mBackgroundAnimator != null) {
687             mBackgroundAnimator.cancel();
688         }
689         mBackgroundDimmed.animate().cancel();
690         mBackgroundNormal.animate().cancel();
691     }
692 
693     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)694     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
695         super.onLayout(changed, left, top, right, bottom);
696         setPivotX(getWidth() / 2);
697     }
698 
699     @Override
setActualHeight(int actualHeight, boolean notifyListeners)700     public void setActualHeight(int actualHeight, boolean notifyListeners) {
701         super.setActualHeight(actualHeight, notifyListeners);
702         setPivotY(actualHeight / 2);
703         mBackgroundNormal.setActualHeight(actualHeight);
704         mBackgroundDimmed.setActualHeight(actualHeight);
705     }
706 
707     @Override
setClipTopAmount(int clipTopAmount)708     public void setClipTopAmount(int clipTopAmount) {
709         super.setClipTopAmount(clipTopAmount);
710         mBackgroundNormal.setClipTopAmount(clipTopAmount);
711         mBackgroundDimmed.setClipTopAmount(clipTopAmount);
712     }
713 
714     @Override
setClipBottomAmount(int clipBottomAmount)715     public void setClipBottomAmount(int clipBottomAmount) {
716         super.setClipBottomAmount(clipBottomAmount);
717         mBackgroundNormal.setClipBottomAmount(clipBottomAmount);
718         mBackgroundDimmed.setClipBottomAmount(clipBottomAmount);
719     }
720 
721     @Override
performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)722     public long performRemoveAnimation(long duration, long delay,
723             float translationDirection, boolean isHeadsUpAnimation, float endLocation,
724             Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener) {
725         enableAppearDrawing(true);
726         mIsHeadsUpAnimation = isHeadsUpAnimation;
727         mHeadsUpLocation = endLocation;
728         if (mDrawingAppearAnimation) {
729             startAppearAnimation(false /* isAppearing */, translationDirection,
730                     delay, duration, onFinishedRunnable, animationListener);
731         } else if (onFinishedRunnable != null) {
732             onFinishedRunnable.run();
733         }
734         return 0;
735     }
736 
737     @Override
performAddAnimation(long delay, long duration, boolean isHeadsUpAppear)738     public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) {
739         enableAppearDrawing(true);
740         mIsHeadsUpAnimation = isHeadsUpAppear;
741         mHeadsUpLocation = mHeadsUpAddStartLocation;
742         if (mDrawingAppearAnimation) {
743             startAppearAnimation(true /* isAppearing */, isHeadsUpAppear ? 0.0f : -1.0f, delay,
744                     duration, null, null);
745         }
746     }
747 
startAppearAnimation(boolean isAppearing, float translationDirection, long delay, long duration, final Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)748     private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay,
749             long duration, final Runnable onFinishedRunnable,
750             AnimatorListenerAdapter animationListener) {
751         cancelAppearAnimation();
752         mAnimationTranslationY = translationDirection * getActualHeight();
753         if (mAppearAnimationFraction == -1.0f) {
754             // not initialized yet, we start anew
755             if (isAppearing) {
756                 mAppearAnimationFraction = 0.0f;
757                 mAppearAnimationTranslation = mAnimationTranslationY;
758             } else {
759                 mAppearAnimationFraction = 1.0f;
760                 mAppearAnimationTranslation = 0;
761             }
762         }
763         mIsAppearing = isAppearing;
764 
765         float targetValue;
766         if (isAppearing) {
767             mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
768             mCurrentAlphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN;
769             targetValue = 1.0f;
770         } else {
771             mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN;
772             mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
773             targetValue = 0.0f;
774         }
775         mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
776                 targetValue);
777         mAppearAnimator.setInterpolator(Interpolators.LINEAR);
778         mAppearAnimator.setDuration(
779                 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue)));
780         mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
781             @Override
782             public void onAnimationUpdate(ValueAnimator animation) {
783                 mAppearAnimationFraction = (float) animation.getAnimatedValue();
784                 updateAppearAnimationAlpha();
785                 updateAppearRect();
786                 invalidate();
787             }
788         });
789         if (animationListener != null) {
790             mAppearAnimator.addListener(animationListener);
791         }
792         if (delay > 0) {
793             // we need to apply the initial state already to avoid drawn frames in the wrong state
794             updateAppearAnimationAlpha();
795             updateAppearRect();
796             mAppearAnimator.setStartDelay(delay);
797         }
798         mAppearAnimator.addListener(new AnimatorListenerAdapter() {
799             private boolean mWasCancelled;
800 
801             @Override
802             public void onAnimationEnd(Animator animation) {
803                 if (onFinishedRunnable != null) {
804                     onFinishedRunnable.run();
805                 }
806                 if (!mWasCancelled) {
807                     enableAppearDrawing(false);
808                     onAppearAnimationFinished(isAppearing);
809                 }
810             }
811 
812             @Override
813             public void onAnimationStart(Animator animation) {
814                 mWasCancelled = false;
815             }
816 
817             @Override
818             public void onAnimationCancel(Animator animation) {
819                 mWasCancelled = true;
820             }
821         });
822         mAppearAnimator.start();
823     }
824 
onAppearAnimationFinished(boolean wasAppearing)825     protected void onAppearAnimationFinished(boolean wasAppearing) {
826     }
827 
cancelAppearAnimation()828     private void cancelAppearAnimation() {
829         if (mAppearAnimator != null) {
830             mAppearAnimator.cancel();
831             mAppearAnimator = null;
832         }
833     }
834 
cancelAppearDrawing()835     public void cancelAppearDrawing() {
836         cancelAppearAnimation();
837         enableAppearDrawing(false);
838     }
839 
updateAppearRect()840     private void updateAppearRect() {
841         float inverseFraction = (1.0f - mAppearAnimationFraction);
842         float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
843         float translateYTotalAmount = translationFraction * mAnimationTranslationY;
844         mAppearAnimationTranslation = translateYTotalAmount;
845 
846         // handle width animation
847         float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
848                 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
849         widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
850         widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
851         float startWidthFraction = HORIZONTAL_COLLAPSED_REST_PARTIAL;
852         if (mIsHeadsUpAnimation && !mIsAppearing) {
853             startWidthFraction = 0;
854         }
855         float width = MathUtils.lerp(startWidthFraction, 1.0f, 1.0f - widthFraction)
856                         * getWidth();
857         float left;
858         float right;
859         if (mIsHeadsUpAnimation) {
860             left = MathUtils.lerp(mHeadsUpLocation, 0, 1.0f - widthFraction);
861             right = left + width;
862         } else {
863             left = getWidth() * 0.5f - width / 2.0f;
864             right = getWidth() - left;
865         }
866 
867         // handle top animation
868         float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
869                 VERTICAL_ANIMATION_START;
870         heightFraction = Math.max(0.0f, heightFraction);
871         heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
872 
873         float top;
874         float bottom;
875         final int actualHeight = getActualHeight();
876         if (mAnimationTranslationY > 0.0f) {
877             bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f
878                     - translateYTotalAmount;
879             top = bottom * heightFraction;
880         } else {
881             top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f -
882                     translateYTotalAmount;
883             bottom = actualHeight * (1 - heightFraction) + top * heightFraction;
884         }
885         mAppearAnimationRect.set(left, top, right, bottom);
886         setOutlineRect(left, top + mAppearAnimationTranslation, right,
887                 bottom + mAppearAnimationTranslation);
888     }
889 
updateAppearAnimationAlpha()890     private void updateAppearAnimationAlpha() {
891         float contentAlphaProgress = mAppearAnimationFraction;
892         contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
893         contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
894         contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
895         setContentAlpha(contentAlphaProgress);
896     }
897 
setContentAlpha(float contentAlpha)898     private void setContentAlpha(float contentAlpha) {
899         View contentView = getContentView();
900         if (contentView.hasOverlappingRendering()) {
901             int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE
902                     : LAYER_TYPE_HARDWARE;
903             int currentLayerType = contentView.getLayerType();
904             if (currentLayerType != layerType) {
905                 contentView.setLayerType(layerType, null);
906             }
907         }
908         contentView.setAlpha(contentAlpha);
909     }
910 
911     @Override
applyRoundness()912     protected void applyRoundness() {
913         super.applyRoundness();
914         applyBackgroundRoundness(getCurrentBackgroundRadiusTop(),
915                 getCurrentBackgroundRadiusBottom());
916     }
917 
applyBackgroundRoundness(float topRadius, float bottomRadius)918     protected void applyBackgroundRoundness(float topRadius, float bottomRadius) {
919         mBackgroundDimmed.setRoundness(topRadius, bottomRadius);
920         mBackgroundNormal.setRoundness(topRadius, bottomRadius);
921     }
922 
923     @Override
setBackgroundTop(int backgroundTop)924     protected void setBackgroundTop(int backgroundTop) {
925         mBackgroundDimmed.setBackgroundTop(backgroundTop);
926         mBackgroundNormal.setBackgroundTop(backgroundTop);
927     }
928 
getContentView()929     protected abstract View getContentView();
930 
calculateBgColor()931     public int calculateBgColor() {
932         return calculateBgColor(true /* withTint */, true /* withOverRide */);
933     }
934 
935     @Override
childNeedsClipping(View child)936     protected boolean childNeedsClipping(View child) {
937         if (child instanceof NotificationBackgroundView && isClippingNeeded()) {
938             return true;
939         }
940         return super.childNeedsClipping(child);
941     }
942 
943     /**
944      * @param withTint should a possible tint be factored in?
945      * @param withOverride should the value be interpolated with {@link #mOverrideTint}
946      * @return the calculated background color
947      */
calculateBgColor(boolean withTint, boolean withOverride)948     private int calculateBgColor(boolean withTint, boolean withOverride) {
949         if (withOverride && mOverrideTint != NO_COLOR) {
950             int defaultTint = calculateBgColor(withTint, false);
951             return NotificationUtils.interpolateColors(defaultTint, mOverrideTint, mOverrideAmount);
952         }
953         if (withTint && mBgTint != NO_COLOR) {
954             return mBgTint;
955         } else {
956             return mNormalColor;
957         }
958     }
959 
getRippleColor()960     protected int getRippleColor() {
961         if (mBgTint != 0) {
962             return mTintedRippleColor;
963         } else {
964             return mNormalRippleColor;
965         }
966     }
967 
968     /**
969      * When we draw the appear animation, we render the view in a bitmap and render this bitmap
970      * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
971      * such that the normal drawing of the views does not happen anymore.
972      *
973      * @param enable Should it be enabled.
974      */
enableAppearDrawing(boolean enable)975     private void enableAppearDrawing(boolean enable) {
976         if (enable != mDrawingAppearAnimation) {
977             mDrawingAppearAnimation = enable;
978             if (!enable) {
979                 setContentAlpha(1.0f);
980                 mAppearAnimationFraction = -1;
981                 setOutlineRect(null);
982             }
983             invalidate();
984         }
985     }
986 
isDrawingAppearAnimation()987     public boolean isDrawingAppearAnimation() {
988         return mDrawingAppearAnimation;
989     }
990 
991     @Override
dispatchDraw(Canvas canvas)992     protected void dispatchDraw(Canvas canvas) {
993         if (mDrawingAppearAnimation) {
994             canvas.save();
995             canvas.translate(0, mAppearAnimationTranslation);
996         }
997         super.dispatchDraw(canvas);
998         if (mDrawingAppearAnimation) {
999             canvas.restore();
1000         }
1001     }
1002 
setOnActivatedListener(OnActivatedListener onActivatedListener)1003     public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
1004         mOnActivatedListener = onActivatedListener;
1005     }
1006 
hasSameBgColor(ActivatableNotificationView otherView)1007     public boolean hasSameBgColor(ActivatableNotificationView otherView) {
1008         return calculateBgColor() == otherView.calculateBgColor();
1009     }
1010 
1011     @Override
setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, int outlineTranslation)1012     public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd,
1013             int outlineTranslation) {
1014         boolean hiddenBefore = mShadowHidden;
1015         mShadowHidden = shadowIntensity == 0.0f;
1016         if (!mShadowHidden || !hiddenBefore) {
1017             mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (getTranslationZ()
1018                             + FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd,
1019                     outlineTranslation);
1020         }
1021     }
1022 
getBackgroundColorWithoutTint()1023     public int getBackgroundColorWithoutTint() {
1024         return calculateBgColor(false /* withTint */, false /* withOverride */);
1025     }
1026 
getCurrentBackgroundTint()1027     public int getCurrentBackgroundTint() {
1028         return mCurrentBackgroundTint;
1029     }
1030 
isPinned()1031     public boolean isPinned() {
1032         return false;
1033     }
1034 
isHeadsUpAnimatingAway()1035     public boolean isHeadsUpAnimatingAway() {
1036         return false;
1037     }
1038 
isHeadsUp()1039     public boolean isHeadsUp() {
1040         return false;
1041     }
1042 
getHeadsUpHeightWithoutHeader()1043     public int getHeadsUpHeightWithoutHeader() {
1044         return getHeight();
1045     }
1046 
1047     public interface OnActivatedListener {
onActivated(ActivatableNotificationView view)1048         void onActivated(ActivatableNotificationView view);
onActivationReset(ActivatableNotificationView view)1049         void onActivationReset(ActivatableNotificationView view);
1050     }
1051 }
1052