ActivatableNotificationView.java revision 98fb09c2b2dbf57803a8737ee7b73cf167721312
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) mBackgroundAnimator.getCurrentPlayTime();
234            mBackgroundAnimator.removeAllListeners();
235            mBackgroundAnimator.cancel();
236            if (duration <= 0) {
237                updateBackgroundResource();
238                return;
239            }
240        }
241        mBackgroundNormal.setAlpha(startAlpha);
242        mBackgroundAnimator =
243                ObjectAnimator.ofInt(mBackgroundNormal, "alpha", startAlpha, endAlpha);
244        mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator);
245        mBackgroundAnimator.setDuration(duration);
246        mBackgroundAnimator.addListener(new AnimatorListenerAdapter() {
247            @Override
248            public void onAnimationEnd(Animator animation) {
249                if (mDimmed) {
250                    setBackgroundNormal(null);
251                } else {
252                    setBackgroundDimmed(null);
253                }
254                mBackgroundAnimator = null;
255            }
256        });
257        mBackgroundAnimator.start();
258    }
259
260    private void updateBackgroundResource() {
261        if (mDimmed) {
262            setBackgroundDimmed(mDimmedBgResId);
263            mBackgroundDimmed.setAlpha(255);
264            setBackgroundNormal(null);
265        } else {
266            setBackgroundDimmed(null);
267            setBackgroundNormal(mBgResId);
268            mBackgroundNormal.setAlpha(255);
269        }
270    }
271
272    /**
273     * Sets a background drawable for the normal state. As we need to change our bounds
274     * independently of layout, we need the notion of a background independently of the regular View
275     * background..
276     */
277    private void setBackgroundNormal(Drawable backgroundNormal) {
278        if (mBackgroundNormal != null) {
279            mBackgroundNormal.setCallback(null);
280            unscheduleDrawable(mBackgroundNormal);
281        }
282        mBackgroundNormal = backgroundNormal;
283        if (mBackgroundNormal != null) {
284            mBackgroundNormal.setCallback(this);
285        }
286        invalidate();
287    }
288
289    private void setBackgroundDimmed(Drawable overlay) {
290        if (mBackgroundDimmed != null) {
291            mBackgroundDimmed.setCallback(null);
292            unscheduleDrawable(mBackgroundDimmed);
293        }
294        mBackgroundDimmed = overlay;
295        if (mBackgroundDimmed != null) {
296            mBackgroundDimmed.setCallback(this);
297        }
298        invalidate();
299    }
300
301    private void setBackgroundNormal(int drawableResId) {
302        setBackgroundNormal(getResources().getDrawable(drawableResId));
303    }
304
305    private void setBackgroundDimmed(int drawableResId) {
306        setBackgroundDimmed(getResources().getDrawable(drawableResId));
307    }
308
309    @Override
310    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
311        super.onLayout(changed, left, top, right, bottom);
312        setPivotX(getWidth() / 2);
313    }
314
315    @Override
316    public void setActualHeight(int actualHeight) {
317        super.setActualHeight(actualHeight);
318        invalidate();
319        setPivotY(actualHeight / 2);
320    }
321
322    @Override
323    public void setClipTopAmount(int clipTopAmount) {
324        super.setClipTopAmount(clipTopAmount);
325        invalidate();
326    }
327
328    public void setOnActivatedListener(OnActivatedListener onActivatedListener) {
329        mOnActivatedListener = onActivatedListener;
330    }
331
332    public interface OnActivatedListener {
333        void onActivated(View view);
334        void onReset(View view);
335    }
336}
337