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