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