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