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