ActivatableNotificationView.java revision 4222d9a7fb87d73e1443ec1a2de9782b05741af6
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.content.Context;
23import android.graphics.Canvas;
24import android.graphics.drawable.Drawable;
25import android.util.AttributeSet;
26import android.view.MotionEvent;
27import android.view.View;
28import android.view.ViewConfiguration;
29import android.view.animation.AnimationUtils;
30import android.view.animation.Interpolator;
31
32import com.android.internal.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 = 1000;
41
42    private boolean mDimmed;
43
44    private int mBgResId = R.drawable.notification_quantum_bg;
45    private int mDimmedBgResId = R.drawable.notification_quantum_bg_dim;
46
47    /**
48     * Flag to indicate that the notification has been touched once and the second touch will
49     * click it.
50     */
51    private boolean mActivated;
52
53    private float mDownX;
54    private float mDownY;
55    private final float mTouchSlop;
56
57    private OnActivatedListener mOnActivatedListener;
58
59    protected Drawable mBackgroundNormal;
60    protected Drawable mBackgroundDimmed;
61    private ObjectAnimator mBackgroundAnimator;
62    private Interpolator mFastOutSlowInInterpolator;
63
64    public ActivatableNotificationView(Context context, AttributeSet attrs) {
65        super(context, attrs);
66        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
67        updateBackgroundResource();
68        setWillNotDraw(false);
69        mFastOutSlowInInterpolator =
70                AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
71    }
72
73    private final Runnable mTapTimeoutRunnable = new Runnable() {
74        @Override
75        public void run() {
76            makeInactive();
77        }
78    };
79
80    @Override
81    protected void onDraw(Canvas canvas) {
82        draw(canvas, mBackgroundNormal);
83        draw(canvas, mBackgroundDimmed);
84    }
85
86    private void draw(Canvas canvas, Drawable drawable) {
87        if (drawable != null) {
88            drawable.setBounds(0, mClipTopAmount, getWidth(), mActualHeight);
89            drawable.draw(canvas);
90        }
91    }
92
93    @Override
94    protected boolean verifyDrawable(Drawable who) {
95        return super.verifyDrawable(who) || who == mBackgroundNormal
96                || who == mBackgroundDimmed;
97    }
98
99    @Override
100    protected void drawableStateChanged() {
101        drawableStateChanged(mBackgroundNormal);
102        drawableStateChanged(mBackgroundDimmed);
103    }
104
105    private void drawableStateChanged(Drawable d) {
106        if (d != null && d.isStateful()) {
107            d.setState(getDrawableState());
108        }
109    }
110
111    @Override
112    public void setOnClickListener(OnClickListener l) {
113        super.setOnClickListener(l);
114    }
115
116    @Override
117    public boolean onTouchEvent(MotionEvent event) {
118        if (mDimmed) {
119            return handleTouchEventDimmed(event);
120        } else {
121            return super.onTouchEvent(event);
122        }
123    }
124
125    private boolean handleTouchEventDimmed(MotionEvent event) {
126        int action = event.getActionMasked();
127        switch (action) {
128            case MotionEvent.ACTION_DOWN:
129                mDownX = event.getX();
130                mDownY = event.getY();
131                if (mDownY > getActualHeight()) {
132                    return false;
133                }
134                break;
135            case MotionEvent.ACTION_MOVE:
136                if (!isWithinTouchSlop(event)) {
137                    makeInactive();
138                    return false;
139                }
140                break;
141            case MotionEvent.ACTION_UP:
142                if (isWithinTouchSlop(event)) {
143                    if (!mActivated) {
144                        makeActive(event.getX(), event.getY());
145                        postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS);
146                    } else {
147                        makeInactive();
148                        performClick();
149                    }
150                } else {
151                    makeInactive();
152                }
153                break;
154            case MotionEvent.ACTION_CANCEL:
155                makeInactive();
156                break;
157            default:
158                break;
159        }
160        return true;
161    }
162
163    private void makeActive(float x, float y) {
164        mBackgroundDimmed.setHotspot(0, x, y);
165        mActivated = true;
166        if (mOnActivatedListener != null) {
167            mOnActivatedListener.onActivated(this);
168        }
169    }
170
171    /**
172     * Cancels the hotspot and makes the notification inactive.
173     */
174    private void makeInactive() {
175        if (mActivated) {
176            // Make sure that we clear the hotspot from the center.
177            mBackgroundDimmed.setHotspot(0, getWidth() / 2, getActualHeight() / 2);
178            mBackgroundDimmed.removeHotspot(0);
179            mActivated = false;
180        }
181        if (mOnActivatedListener != null) {
182            mOnActivatedListener.onReset(this);
183        }
184        removeCallbacks(mTapTimeoutRunnable);
185    }
186
187    private boolean isWithinTouchSlop(MotionEvent event) {
188        return Math.abs(event.getX() - mDownX) < mTouchSlop
189                && Math.abs(event.getY() - mDownY) < mTouchSlop;
190    }
191
192    /**
193     * Sets the notification as dimmed, meaning that it will appear in a more gray variant.
194     *
195     * @param dimmed Whether the notification should be dimmed.
196     * @param fade Whether an animation should be played to change the state.
197     */
198    public void setDimmed(boolean dimmed, boolean fade) {
199        if (mDimmed != dimmed) {
200            mDimmed = dimmed;
201            if (fade) {
202                fadeBackgroundResource();
203            } else {
204                updateBackgroundResource();
205            }
206        }
207    }
208
209    /**
210     * Sets the resource id for the background of this notification.
211     *
212     * @param bgResId The background resource to use in normal state.
213     * @param dimmedBgResId The background resource to use in dimmed state.
214     */
215    public void setBackgroundResourceIds(int bgResId, int dimmedBgResId) {
216        mBgResId = bgResId;
217        mDimmedBgResId = dimmedBgResId;
218        updateBackgroundResource();
219    }
220
221    private void fadeBackgroundResource() {
222        if (mDimmed) {
223            setBackgroundDimmed(mDimmedBgResId);
224        } else {
225            setBackgroundNormal(mBgResId);
226        }
227        int startAlpha = mDimmed ? 255 : 0;
228        int endAlpha = mDimmed ? 0 : 255;
229        int duration = NotificationActivator.ANIMATION_LENGTH_MS;
230        // Check whether there is already a background animation running.
231        if (mBackgroundAnimator != null) {
232            startAlpha = (Integer) mBackgroundAnimator.getAnimatedValue();
233            duration = (int) (NotificationActivator.ANIMATION_LENGTH_MS
234                                - mBackgroundAnimator.getCurrentPlayTime());
235            mBackgroundAnimator.removeAllListeners();
236            mBackgroundAnimator.cancel();
237        }
238        mBackgroundNormal.setAlpha(startAlpha);
239        mBackgroundAnimator =
240                ObjectAnimator.ofInt(mBackgroundNormal, "alpha", startAlpha, endAlpha);
241        mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator);
242        mBackgroundAnimator.setDuration(duration);
243        mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
244            @Override
245            public void onAnimationEnd(Animator animation) {
246                if (mDimmed) {
247                    setBackgroundNormal(null);
248                } else {
249                    setBackgroundDimmed(null);
250                }
251                mBackgroundAnimator = null;
252            }
253        });
254        mBackgroundAnimator.start();
255    }
256
257    private void updateBackgroundResource() {
258        if (mDimmed) {
259            setBackgroundDimmed(mDimmedBgResId);
260            setBackgroundNormal(null);
261        } else {
262            setBackgroundDimmed(null);
263            setBackgroundNormal(mBgResId);
264        }
265    }
266
267    /**
268     * Sets a background drawable for the normal state. As we need to change our bounds
269     * independently of layout, we need the notion of a background independently of the regular View
270     * background..
271     */
272    private void setBackgroundNormal(Drawable backgroundNormal) {
273        if (mBackgroundNormal != null) {
274            mBackgroundNormal.setCallback(null);
275            unscheduleDrawable(mBackgroundNormal);
276        }
277        mBackgroundNormal = backgroundNormal;
278        if (mBackgroundNormal != null) {
279            mBackgroundNormal.setCallback(this);
280        }
281        invalidate();
282    }
283
284    private void setBackgroundDimmed(Drawable overlay) {
285        if (mBackgroundDimmed != null) {
286            mBackgroundDimmed.setCallback(null);
287            unscheduleDrawable(mBackgroundDimmed);
288        }
289        mBackgroundDimmed = overlay;
290        if (mBackgroundDimmed != null) {
291            mBackgroundDimmed.setCallback(this);
292        }
293        invalidate();
294    }
295
296    private void setBackgroundNormal(int drawableResId) {
297        setBackgroundNormal(getResources().getDrawable(drawableResId));
298    }
299
300    private void setBackgroundDimmed(int drawableResId) {
301        setBackgroundDimmed(getResources().getDrawable(drawableResId));
302    }
303
304    @Override
305    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
306        super.onLayout(changed, left, top, right, bottom);
307        setPivotX(getWidth() / 2);
308    }
309
310    @Override
311    public void setActualHeight(int actualHeight) {
312        super.setActualHeight(actualHeight);
313        invalidate();
314        setPivotY(actualHeight / 2);
315    }
316
317    @Override
318    public void setClipTopAmount(int clipTopAmount) {
319        super.setClipTopAmount(clipTopAmount);
320        invalidate();
321    }
322
323    public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
324        mOnActivatedListener = onActivatedListener;
325    }
326
327    public interface OnActivatedListener {
328        void onActivated(View view);
329        void onReset(View view);
330    }
331}
332