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