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