ActivatableNotificationView.java revision 3cb07a462be293634e6a83ea6c82f3647cd17dad
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;
44import com.android.systemui.statusbar.stack.StackStateAnimator;
45
46/**
47 * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer}
48 * to implement dimming/activating on Keyguard for the double-tap gesture
49 */
50public abstract class ActivatableNotificationView extends ExpandableOutlineView {
51
52    private static final long DOUBLETAP_TIMEOUT_MS = 1200;
53    private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
54    private static final int ACTIVATE_ANIMATION_LENGTH = 220;
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    private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
87            = new PathInterpolator(0.6f, 0, 0.5f, 1);
88    private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
89            = new PathInterpolator(0, 0, 0.5f, 1);
90
91    private boolean mDimmed;
92
93    private int mBgResId = com.android.internal.R.drawable.notification_material_bg;
94    private int mDimmedBgResId = com.android.internal.R.drawable.notification_material_bg_dim;
95
96    private int mBgTint = 0;
97    private int mDimmedBgTint = 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 ObjectAnimator mBackgroundAnimator;
123    private RectF mAppearAnimationRect = new RectF();
124    private PorterDuffColorFilter mAppearAnimationFilter;
125    private float mAnimationTranslationY;
126    private boolean mDrawingAppearAnimation;
127    private Paint mAppearPaint = new Paint();
128    private ValueAnimator mAppearAnimator;
129    private float mAppearAnimationFraction = -1.0f;
130    private float mAppearAnimationTranslation;
131
132    public ActivatableNotificationView(Context context, AttributeSet attrs) {
133        super(context, attrs);
134        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
135        mFastOutSlowInInterpolator =
136                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
137        mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f);
138        mLinearOutSlowInInterpolator =
139                AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
140        mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f);
141        mLinearInterpolator = new LinearInterpolator();
142        setClipChildren(false);
143        setClipToPadding(false);
144        mAppearAnimationFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
145        mRoundedRectCornerRadius = getResources().getDimensionPixelSize(
146                com.android.internal.R.dimen.notification_material_rounded_rect_radius);
147    }
148
149    @Override
150    protected void onFinishInflate() {
151        super.onFinishInflate();
152        mBackgroundNormal = (NotificationBackgroundView) findViewById(R.id.backgroundNormal);
153        mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed);
154        updateBackground();
155        updateBackgroundResources();
156    }
157
158    private final Runnable mTapTimeoutRunnable = new Runnable() {
159        @Override
160        public void run() {
161            makeInactive();
162        }
163    };
164
165    @Override
166    public boolean onTouchEvent(MotionEvent event) {
167        if (mDimmed) {
168            return handleTouchEventDimmed(event);
169        } else {
170            return super.onTouchEvent(event);
171        }
172    }
173
174    private boolean handleTouchEventDimmed(MotionEvent event) {
175        int action = event.getActionMasked();
176        switch (action) {
177            case MotionEvent.ACTION_DOWN:
178                mDownX = event.getX();
179                mDownY = event.getY();
180                if (mDownY > getActualHeight()) {
181                    return false;
182                }
183                break;
184            case MotionEvent.ACTION_MOVE:
185                if (!isWithinTouchSlop(event)) {
186                    makeInactive();
187                    return false;
188                }
189                break;
190            case MotionEvent.ACTION_UP:
191                if (isWithinTouchSlop(event)) {
192                    if (!mActivated) {
193                        makeActive();
194                        postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
195                    } else {
196                        performClick();
197                    }
198                } else {
199                    makeInactive();
200                }
201                break;
202            case MotionEvent.ACTION_CANCEL:
203                makeInactive();
204                break;
205            default:
206                break;
207        }
208        return true;
209    }
210
211    private void makeActive() {
212        startActivateAnimation(false /* reverse */);
213        mActivated = true;
214        if (mOnActivatedListener != null) {
215            mOnActivatedListener.onActivated(this);
216        }
217    }
218
219    private void startActivateAnimation(boolean reverse) {
220        int widthHalf = mBackgroundNormal.getWidth()/2;
221        int heightHalf = mBackgroundNormal.getActualHeight()/2;
222        float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf);
223        ValueAnimator animator =
224                ViewAnimationUtils.createCircularReveal(mBackgroundNormal,
225                        widthHalf, heightHalf, 0, radius);
226        mBackgroundNormal.setVisibility(View.VISIBLE);
227        Interpolator interpolator;
228        Interpolator alphaInterpolator;
229        if (!reverse) {
230            interpolator = mLinearOutSlowInInterpolator;
231            alphaInterpolator = mLinearOutSlowInInterpolator;
232        } else {
233            interpolator = ACTIVATE_INVERSE_INTERPOLATOR;
234            alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR;
235        }
236        animator.setInterpolator(interpolator);
237        animator.setDuration(ACTIVATE_ANIMATION_LENGTH);
238        if (reverse) {
239            mBackgroundNormal.setAlpha(1f);
240            animator.addListener(new AnimatorListenerAdapter() {
241                @Override
242                public void onAnimationEnd(Animator animation) {
243                    mBackgroundNormal.setVisibility(View.INVISIBLE);
244                }
245            });
246            animator.reverse();
247        } else {
248            mBackgroundNormal.setAlpha(0.4f);
249            animator.start();
250        }
251        mBackgroundNormal.animate()
252                .alpha(reverse ? 0f : 1f)
253                .setInterpolator(alphaInterpolator)
254                .setDuration(ACTIVATE_ANIMATION_LENGTH);
255    }
256
257    /**
258     * Cancels the hotspot and makes the notification inactive.
259     */
260    private void makeInactive() {
261        if (mActivated) {
262            if (mDimmed) {
263                startActivateAnimation(true /* reverse */);
264            }
265            mActivated = false;
266        }
267        if (mOnActivatedListener != null) {
268            mOnActivatedListener.onActivationReset(this);
269        }
270        removeCallbacks(mTapTimeoutRunnable);
271    }
272
273    private boolean isWithinTouchSlop(MotionEvent event) {
274        return Math.abs(event.getX() - mDownX) < mTouchSlop
275                && Math.abs(event.getY() - mDownY) < mTouchSlop;
276    }
277
278    public void setDimmed(boolean dimmed, boolean fade) {
279        if (mDimmed != dimmed) {
280            mDimmed = dimmed;
281            if (fade) {
282                fadeBackground();
283            } else {
284                updateBackground();
285            }
286        }
287    }
288
289    /**
290     * Sets the resource id for the background of this notification.
291     *
292     * @param bgResId The background resource to use in normal state.
293     * @param dimmedBgResId The background resource to use in dimmed state.
294     */
295    public void setBackgroundResourceIds(int bgResId, int bgTint, int dimmedBgResId, int dimmedTint) {
296        mBgResId = bgResId;
297        mBgTint = bgTint;
298        mDimmedBgResId = dimmedBgResId;
299        mDimmedBgTint = dimmedTint;
300        updateBackgroundResources();
301    }
302
303    public void setBackgroundResourceIds(int bgResId, int dimmedBgResId) {
304        setBackgroundResourceIds(bgResId, 0, dimmedBgResId, 0);
305    }
306
307    private void fadeBackground() {
308        if (mDimmed) {
309            mBackgroundDimmed.setVisibility(View.VISIBLE);
310        } else {
311            mBackgroundNormal.setVisibility(View.VISIBLE);
312        }
313        float startAlpha = mDimmed ? 1f : 0;
314        float endAlpha = mDimmed ? 0 : 1f;
315        int duration = BACKGROUND_ANIMATION_LENGTH_MS;
316        // Check whether there is already a background animation running.
317        if (mBackgroundAnimator != null) {
318            startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
319            duration = (int) mBackgroundAnimator.getCurrentPlayTime();
320            mBackgroundAnimator.removeAllListeners();
321            mBackgroundAnimator.cancel();
322            if (duration <= 0) {
323                updateBackground();
324                return;
325            }
326        }
327        mBackgroundNormal.setAlpha(startAlpha);
328        mBackgroundAnimator =
329                ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
330        mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator);
331        mBackgroundAnimator.setDuration(duration);
332        mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
333            @Override
334            public void onAnimationEnd(Animator animation) {
335                if (mDimmed) {
336                    mBackgroundNormal.setVisibility(View.INVISIBLE);
337                } else {
338                    mBackgroundDimmed.setVisibility(View.INVISIBLE);
339                }
340                mBackgroundAnimator = null;
341            }
342        });
343        mBackgroundAnimator.start();
344    }
345
346    private void updateBackground() {
347        if (mDimmed) {
348            mBackgroundDimmed.setVisibility(View.VISIBLE);
349            mBackgroundNormal.setVisibility(View.INVISIBLE);
350        } else {
351            mBackgroundDimmed.setVisibility(View.INVISIBLE);
352            mBackgroundNormal.setVisibility(View.VISIBLE);
353            mBackgroundNormal.setAlpha(1f);
354        }
355    }
356
357    private void updateBackgroundResources() {
358        mBackgroundDimmed.setCustomBackground(mDimmedBgResId, mDimmedBgTint);
359        mBackgroundNormal.setCustomBackground(mBgResId, mBgTint);
360    }
361
362    @Override
363    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
364        super.onLayout(changed, left, top, right, bottom);
365        setPivotX(getWidth() / 2);
366    }
367
368    @Override
369    public void setActualHeight(int actualHeight, boolean notifyListeners) {
370        super.setActualHeight(actualHeight, notifyListeners);
371        setPivotY(actualHeight / 2);
372        mBackgroundNormal.setActualHeight(actualHeight);
373        mBackgroundDimmed.setActualHeight(actualHeight);
374    }
375
376    @Override
377    public void setClipTopAmount(int clipTopAmount) {
378        super.setClipTopAmount(clipTopAmount);
379        mBackgroundNormal.setClipTopAmount(clipTopAmount);
380        mBackgroundDimmed.setClipTopAmount(clipTopAmount);
381    }
382
383    @Override
384    public void performRemoveAnimation(float translationDirection, Runnable onFinishedRunnable) {
385        enableAppearDrawing(true);
386        if (mDrawingAppearAnimation) {
387            startAppearAnimation(false /* isAppearing */, translationDirection,
388                    0, onFinishedRunnable);
389        }
390    }
391
392    @Override
393    public void performAddAnimation(long delay) {
394        enableAppearDrawing(true);
395        if (mDrawingAppearAnimation) {
396            startAppearAnimation(true /* isAppearing */, -1.0f, delay, null);
397        }
398    }
399
400    private void startAppearAnimation(boolean isAppearing,
401            float translationDirection, long delay, final Runnable onFinishedRunnable) {
402        if (mAppearAnimator != null) {
403            mAppearAnimator.cancel();
404        }
405        mAnimationTranslationY = translationDirection * mActualHeight;
406        if (mAppearAnimationFraction == -1.0f) {
407            // not initialized yet, we start anew
408            if (isAppearing) {
409                mAppearAnimationFraction = 0.0f;
410                mAppearAnimationTranslation = mAnimationTranslationY;
411            } else {
412                mAppearAnimationFraction = 1.0f;
413                mAppearAnimationTranslation = 0;
414            }
415        }
416
417        float targetValue;
418        if (isAppearing) {
419            mCurrentAppearInterpolator = mSlowOutFastInInterpolator;
420            mCurrentAlphaInterpolator = mLinearOutSlowInInterpolator;
421            targetValue = 1.0f;
422        } else {
423            mCurrentAppearInterpolator = mFastOutSlowInInterpolator;
424            mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator;
425            targetValue = 0.0f;
426        }
427        mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction,
428                targetValue);
429        mAppearAnimator.setInterpolator(mLinearInterpolator);
430        mAppearAnimator.setDuration(
431                (long) (StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR
432                        * Math.abs(mAppearAnimationFraction - targetValue)));
433        mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
434            @Override
435            public void onAnimationUpdate(ValueAnimator animation) {
436                mAppearAnimationFraction = (float) animation.getAnimatedValue();
437                updateAppearAnimationAlpha();
438                updateAppearRect();
439                invalidate();
440            }
441        });
442        if (delay > 0) {
443            // we need to apply the initial state already to avoid drawn frames in the wrong state
444            updateAppearAnimationAlpha();
445            updateAppearRect();
446            mAppearAnimator.setStartDelay(delay);
447        }
448        mAppearAnimator.addListener(new AnimatorListenerAdapter() {
449            private boolean mWasCancelled;
450
451            @Override
452            public void onAnimationEnd(Animator animation) {
453                if (onFinishedRunnable != null) {
454                    onFinishedRunnable.run();
455                }
456                if (!mWasCancelled) {
457                    mAppearAnimationFraction = -1;
458                    setOutlineRect(null);
459                    enableAppearDrawing(false);
460                }
461            }
462
463            @Override
464            public void onAnimationStart(Animator animation) {
465                mWasCancelled = false;
466            }
467
468            @Override
469            public void onAnimationCancel(Animator animation) {
470                mWasCancelled = true;
471            }
472        });
473        mAppearAnimator.start();
474    }
475
476    private void updateAppearRect() {
477        float inverseFraction = (1.0f - mAppearAnimationFraction);
478        float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction);
479        float translateYTotalAmount = translationFraction * mAnimationTranslationY;
480        mAppearAnimationTranslation = translateYTotalAmount;
481
482        // handle width animation
483        float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START))
484                / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END);
485        widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction));
486        widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction);
487        float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) *
488                widthFraction);
489        float right = getWidth() - left;
490
491        // handle top animation
492        float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) /
493                VERTICAL_ANIMATION_START;
494        heightFraction = Math.max(0.0f, heightFraction);
495        heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction);
496
497        float top;
498        float bottom;
499        if (mAnimationTranslationY > 0.0f) {
500            bottom = mActualHeight - heightFraction * mAnimationTranslationY * 0.1f
501                    - translateYTotalAmount;
502            top = bottom * heightFraction;
503        } else {
504            top = heightFraction * (mActualHeight + mAnimationTranslationY) * 0.1f -
505                    translateYTotalAmount;
506            bottom = mActualHeight * (1 - heightFraction) + top * heightFraction;
507        }
508        mAppearAnimationRect.set(left, top, right, bottom);
509        setOutlineRect(left, top + mAppearAnimationTranslation, right,
510                bottom + mAppearAnimationTranslation);
511    }
512
513    private void updateAppearAnimationAlpha() {
514        int backgroundColor = getBackgroundColor();
515        if (backgroundColor != -1) {
516            float contentAlphaProgress = mAppearAnimationFraction;
517            contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END);
518            contentAlphaProgress = Math.min(1.0f, contentAlphaProgress);
519            contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress);
520            int sourceColor = Color.argb((int) (255 * (1.0f - contentAlphaProgress)),
521                    Color.red(backgroundColor), Color.green(backgroundColor),
522                    Color.blue(backgroundColor));
523            mAppearAnimationFilter.setColor(sourceColor);
524            mAppearPaint.setColorFilter(mAppearAnimationFilter);
525        }
526    }
527
528    private int getBackgroundColor() {
529        // TODO: get real color
530        return 0xfffafafa;
531    }
532
533    /**
534     * When we draw the appear animation, we render the view in a bitmap and render this bitmap
535     * as a shader of a rect. This call creates the Bitmap and switches the drawing mode,
536     * such that the normal drawing of the views does not happen anymore.
537     *
538     * @param enable Should it be enabled.
539     */
540    private void enableAppearDrawing(boolean enable) {
541        if (enable != mDrawingAppearAnimation) {
542            if (enable) {
543                if (getWidth() == 0 || getActualHeight() == 0) {
544                    // TODO: This should not happen, but it can during expansion. Needs
545                    // investigation
546                    return;
547                }
548                Bitmap bitmap = Bitmap.createBitmap(getWidth(), getActualHeight(),
549                        Bitmap.Config.ARGB_8888);
550                Canvas canvas = new Canvas(bitmap);
551                draw(canvas);
552                mAppearPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP,
553                        Shader.TileMode.CLAMP));
554            } else {
555                mAppearPaint.setShader(null);
556            }
557            mDrawingAppearAnimation = enable;
558            invalidate();
559        }
560    }
561
562    @Override
563    protected void dispatchDraw(Canvas canvas) {
564        if (!mDrawingAppearAnimation) {
565            super.dispatchDraw(canvas);
566        } else {
567            drawAppearRect(canvas);
568        }
569    }
570
571    private void drawAppearRect(Canvas canvas) {
572        canvas.save();
573        canvas.translate(0, mAppearAnimationTranslation);
574        canvas.drawRoundRect(mAppearAnimationRect, mRoundedRectCornerRadius,
575                mRoundedRectCornerRadius, mAppearPaint);
576        canvas.restore();
577    }
578
579    public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
580        mOnActivatedListener = onActivatedListener;
581    }
582
583    public interface OnActivatedListener {
584        void onActivated(View view);
585        void onActivationReset(View view);
586    }
587}
588