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