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