ActivatableNotificationView.java revision 3c3c3fc38c474924629aa591c98d6dc190ed4e83
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.util.AttributeSet;
25import android.view.MotionEvent;
26import android.view.View;
27import android.view.ViewConfiguration;
28import android.view.animation.AnimationUtils;
29import android.view.animation.Interpolator;
30import android.view.animation.PathInterpolator;
31
32import com.android.systemui.R;
33
34/**
35 * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer}
36 * to implement dimming/activating on Keyguard for the double-tap gesture
37 */
38public abstract class ActivatableNotificationView extends ExpandableOutlineView {
39
40    private static final long DOUBLETAP_TIMEOUT_MS = 1200;
41    private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220;
42    private static final int ACTIVATE_ANIMATION_LENGTH = 220;
43
44    private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR
45            = new PathInterpolator(0.6f, 0, 0.5f, 1);
46    private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR
47            = new PathInterpolator(0, 0, 0.5f, 1);
48
49    private boolean mDimmed;
50
51    private int mBgResId = com.android.internal.R.drawable.notification_quantum_bg;
52    private int mDimmedBgResId = com.android.internal.R.drawable.notification_quantum_bg_dim;
53
54    private int mBgTint = 0;
55    private int mDimmedBgTint = 0;
56
57    /**
58     * Flag to indicate that the notification has been touched once and the second touch will
59     * click it.
60     */
61    private boolean mActivated;
62
63    private float mDownX;
64    private float mDownY;
65    private final float mTouchSlop;
66
67    private OnActivatedListener mOnActivatedListener;
68
69    private Interpolator mLinearOutSlowInInterpolator;
70    private Interpolator mFastOutSlowInInterpolator;
71
72    private NotificationBackgroundView mBackgroundNormal;
73    private NotificationBackgroundView mBackgroundDimmed;
74    private ObjectAnimator mBackgroundAnimator;
75
76    public ActivatableNotificationView(Context context, AttributeSet attrs) {
77        super(context, attrs);
78        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
79        mFastOutSlowInInterpolator =
80                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
81        mLinearOutSlowInInterpolator =
82                AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
83        setClipChildren(false);
84        setClipToPadding(false);
85    }
86
87    @Override
88    protected void onFinishInflate() {
89        super.onFinishInflate();
90        mBackgroundNormal = (NotificationBackgroundView) findViewById(R.id.backgroundNormal);
91        mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed);
92        updateBackgroundResources();
93    }
94
95    private final Runnable mTapTimeoutRunnable = new Runnable() {
96        @Override
97        public void run() {
98            makeInactive();
99        }
100    };
101
102    @Override
103    public boolean onTouchEvent(MotionEvent event) {
104        if (mDimmed) {
105            return handleTouchEventDimmed(event);
106        } else {
107            return super.onTouchEvent(event);
108        }
109    }
110
111    private boolean handleTouchEventDimmed(MotionEvent event) {
112        int action = event.getActionMasked();
113        switch (action) {
114            case MotionEvent.ACTION_DOWN:
115                mDownX = event.getX();
116                mDownY = event.getY();
117                if (mDownY > getActualHeight()) {
118                    return false;
119                }
120                break;
121            case MotionEvent.ACTION_MOVE:
122                if (!isWithinTouchSlop(event)) {
123                    makeInactive();
124                    return false;
125                }
126                break;
127            case MotionEvent.ACTION_UP:
128                if (isWithinTouchSlop(event)) {
129                    if (!mActivated) {
130                        makeActive();
131                        postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
132                    } else {
133                        performClick();
134                    }
135                } else {
136                    makeInactive();
137                }
138                break;
139            case MotionEvent.ACTION_CANCEL:
140                makeInactive();
141                break;
142            default:
143                break;
144        }
145        return true;
146    }
147
148    private void makeActive() {
149        startActivateAnimation(false /* reverse */);
150        mActivated = true;
151        if (mOnActivatedListener != null) {
152            mOnActivatedListener.onActivated(this);
153        }
154    }
155
156    private void startActivateAnimation(boolean reverse) {
157        int widthHalf = mBackgroundNormal.getWidth()/2;
158        int heightHalf = mBackgroundNormal.getActualHeight()/2;
159        float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf);
160        ValueAnimator animator =
161                mBackgroundNormal.createRevealAnimator(widthHalf, heightHalf, 0, radius);
162        mBackgroundNormal.setVisibility(View.VISIBLE);
163        Interpolator interpolator;
164        Interpolator alphaInterpolator;
165        if (!reverse) {
166            interpolator = mLinearOutSlowInInterpolator;
167            alphaInterpolator = mLinearOutSlowInInterpolator;
168        } else {
169            interpolator = ACTIVATE_INVERSE_INTERPOLATOR;
170            alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR;
171        }
172        animator.setInterpolator(interpolator);
173        animator.setDuration(ACTIVATE_ANIMATION_LENGTH);
174        if (reverse) {
175            mBackgroundNormal.setAlpha(1f);
176            animator.addListener(new AnimatorListenerAdapter() {
177                @Override
178                public void onAnimationEnd(Animator animation) {
179                    mBackgroundNormal.setVisibility(View.INVISIBLE);
180                }
181            });
182            animator.reverse();
183        } else {
184            mBackgroundNormal.setAlpha(0.4f);
185            animator.start();
186        }
187        mBackgroundNormal.animate()
188                .alpha(reverse ? 0f : 1f)
189                .setInterpolator(alphaInterpolator)
190                .setDuration(ACTIVATE_ANIMATION_LENGTH);
191    }
192
193    /**
194     * Cancels the hotspot and makes the notification inactive.
195     */
196    private void makeInactive() {
197        if (mActivated) {
198            if (mDimmed) {
199                startActivateAnimation(true /* reverse */);
200            }
201            mActivated = false;
202        }
203        if (mOnActivatedListener != null) {
204            mOnActivatedListener.onActivationReset(this);
205        }
206        removeCallbacks(mTapTimeoutRunnable);
207    }
208
209    private boolean isWithinTouchSlop(MotionEvent event) {
210        return Math.abs(event.getX() - mDownX) < mTouchSlop
211                && Math.abs(event.getY() - mDownY) < mTouchSlop;
212    }
213
214    public void setDimmed(boolean dimmed, boolean fade) {
215        if (mDimmed != dimmed) {
216            mDimmed = dimmed;
217            if (fade) {
218                fadeBackground();
219            } else {
220                updateBackground();
221            }
222        }
223    }
224
225    /**
226     * Sets the resource id for the background of this notification.
227     *
228     * @param bgResId The background resource to use in normal state.
229     * @param dimmedBgResId The background resource to use in dimmed state.
230     */
231    public void setBackgroundResourceIds(int bgResId, int bgTint, int dimmedBgResId, int dimmedTint) {
232        mBgResId = bgResId;
233        mBgTint = bgTint;
234        mDimmedBgResId = dimmedBgResId;
235        mDimmedBgTint = dimmedTint;
236        updateBackgroundResources();
237    }
238
239    public void setBackgroundResourceIds(int bgResId, int dimmedBgResId) {
240        setBackgroundResourceIds(bgResId, 0, dimmedBgResId, 0);
241    }
242
243    private void fadeBackground() {
244        if (mDimmed) {
245            mBackgroundDimmed.setVisibility(View.VISIBLE);
246        } else {
247            mBackgroundNormal.setVisibility(View.VISIBLE);
248        }
249        float startAlpha = mDimmed ? 1f : 0;
250        float endAlpha = mDimmed ? 0 : 1f;
251        int duration = BACKGROUND_ANIMATION_LENGTH_MS;
252        // Check whether there is already a background animation running.
253        if (mBackgroundAnimator != null) {
254            startAlpha = (Float) mBackgroundAnimator.getAnimatedValue();
255            duration = (int) mBackgroundAnimator.getCurrentPlayTime();
256            mBackgroundAnimator.removeAllListeners();
257            mBackgroundAnimator.cancel();
258            if (duration <= 0) {
259                updateBackground();
260                return;
261            }
262        }
263        mBackgroundNormal.setAlpha(startAlpha);
264        mBackgroundAnimator =
265                ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha);
266        mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator);
267        mBackgroundAnimator.setDuration(duration);
268        mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
269            @Override
270            public void onAnimationEnd(Animator animation) {
271                if (mDimmed) {
272                    mBackgroundNormal.setVisibility(View.INVISIBLE);
273                } else {
274                    mBackgroundDimmed.setVisibility(View.INVISIBLE);
275                }
276                mBackgroundAnimator = null;
277            }
278        });
279        mBackgroundAnimator.start();
280    }
281
282    private void updateBackground() {
283        if (mDimmed) {
284            mBackgroundDimmed.setVisibility(View.VISIBLE);
285            mBackgroundNormal.setVisibility(View.INVISIBLE);
286        } else {
287            mBackgroundDimmed.setVisibility(View.INVISIBLE);
288            mBackgroundNormal.setVisibility(View.VISIBLE);
289            mBackgroundNormal.setAlpha(1f);
290        }
291    }
292
293    private void updateBackgroundResources() {
294        mBackgroundDimmed.setCustomBackground(mDimmedBgResId, mDimmedBgTint);
295        mBackgroundNormal.setCustomBackground(mBgResId, mBgTint);
296    }
297
298    @Override
299    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
300        super.onLayout(changed, left, top, right, bottom);
301        setPivotX(getWidth() / 2);
302    }
303
304    @Override
305    public void setActualHeight(int actualHeight, boolean notifyListeners) {
306        super.setActualHeight(actualHeight, notifyListeners);
307        setPivotY(actualHeight / 2);
308        mBackgroundNormal.setActualHeight(actualHeight);
309        mBackgroundDimmed.setActualHeight(actualHeight);
310    }
311
312    @Override
313    public void setClipTopAmount(int clipTopAmount) {
314        super.setClipTopAmount(clipTopAmount);
315        mBackgroundNormal.setClipTopAmount(clipTopAmount);
316        mBackgroundDimmed.setClipTopAmount(clipTopAmount);
317    }
318
319    public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
320        mOnActivatedListener = onActivatedListener;
321    }
322
323    public interface OnActivatedListener {
324        void onActivated(View view);
325        void onActivationReset(View view);
326    }
327}
328