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