NotificationPanelView.java revision bff1f071f300e4f99da0fd2bd567ce5f840e5f91
1/*
2 * Copyright (C) 2012 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.phone;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.util.AttributeSet;
24import android.view.MotionEvent;
25import android.view.VelocityTracker;
26import android.view.View;
27import android.view.accessibility.AccessibilityEvent;
28import android.view.animation.AnimationUtils;
29import android.view.animation.Interpolator;
30
31import com.android.systemui.R;
32import com.android.systemui.statusbar.ExpandableView;
33import com.android.systemui.statusbar.GestureRecorder;
34import com.android.systemui.statusbar.StatusBarState;
35import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
36
37public class NotificationPanelView extends PanelView implements
38        ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
39        View.OnClickListener {
40    public static final boolean DEBUG_GESTURES = true;
41    private static final int EXPANSION_ANIMATION_LENGTH = 375;
42
43    PhoneStatusBar mStatusBar;
44    private StatusBarHeaderView mHeader;
45    private QuickSettingsContainerView mQsContainer;
46    private View mKeyguardStatusView;
47    private ObservableScrollView mScrollView;
48    private View mStackScrollerContainer;
49
50    private NotificationStackScrollLayout mNotificationStackScroller;
51    private int mNotificationTopPadding;
52    private boolean mAnimateNextTopPaddingChange;
53
54    private Interpolator mExpansionInterpolator;
55
56    private int mTrackingPointer;
57    private VelocityTracker mVelocityTracker;
58    private boolean mTracking;
59    private boolean mQsExpanded;
60    private float mInitialHeightOnTouch;
61    private float mInitialTouchX;
62    private float mInitialTouchY;
63    private float mQsExpansionHeight;
64    private int mQsMinExpansionHeight;
65    private int mQsMaxExpansionHeight;
66    private int mMinStackHeight;
67    private float mNotificationTranslation;
68    private int mStackScrollerIntrinsicPadding;
69    private boolean mQsExpansionEnabled = true;
70    private ValueAnimator mQsExpansionAnimator;
71
72    public NotificationPanelView(Context context, AttributeSet attrs) {
73        super(context, attrs);
74    }
75
76    public void setStatusBar(PhoneStatusBar bar) {
77        if (mStatusBar != null) {
78            mStatusBar.setOnFlipRunnable(null);
79        }
80        mStatusBar = bar;
81        if (bar != null) {
82            mStatusBar.setOnFlipRunnable(new Runnable() {
83                @Override
84                public void run() {
85                    requestPanelHeightUpdate();
86                }
87            });
88        }
89    }
90
91    @Override
92    protected void onFinishInflate() {
93        super.onFinishInflate();
94        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
95        mHeader.getBackgroundView().setOnClickListener(this);
96        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
97        mStackScrollerContainer = findViewById(R.id.notification_container_parent);
98        mQsContainer = (QuickSettingsContainerView) findViewById(R.id.quick_settings_container);
99        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
100        mScrollView.setListener(this);
101        mNotificationStackScroller = (NotificationStackScrollLayout)
102                findViewById(R.id.notification_stack_scroller);
103        mNotificationStackScroller.setOnHeightChangedListener(this);
104        mNotificationTopPadding = getResources().getDimensionPixelSize(
105                R.dimen.notifications_top_padding);
106        mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height);
107        mExpansionInterpolator = AnimationUtils.loadInterpolator(
108                getContext(), android.R.interpolator.fast_out_slow_in);
109    }
110
111    @Override
112    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
113        super.onLayout(changed, left, top, right, bottom);
114        int keyguardBottomMargin =
115                ((MarginLayoutParams) mKeyguardStatusView.getLayoutParams()).bottomMargin;
116        if (!mQsExpanded) {
117            mStackScrollerIntrinsicPadding = mStatusBar.getBarState() == StatusBarState.KEYGUARD
118                    ? mKeyguardStatusView.getBottom() + keyguardBottomMargin
119                    : mHeader.getBottom() + mNotificationTopPadding;
120            mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding,
121                    mAnimateNextTopPaddingChange);
122            mAnimateNextTopPaddingChange = false;
123        }
124
125        // Calculate quick setting heights.
126        mQsMinExpansionHeight = mHeader.getCollapsedHeight();
127        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
128        if (mQsExpansionHeight == 0) {
129            mQsExpansionHeight = mQsMinExpansionHeight;
130        }
131    }
132
133    public void animateNextTopPaddingChange() {
134        mAnimateNextTopPaddingChange = true;
135        requestLayout();
136    }
137
138    /**
139     * @return Whether Quick Settings are currently expanded.
140     */
141    public boolean isQsExpanded() {
142        return mQsExpanded;
143    }
144
145    public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
146        mQsExpansionEnabled = qsExpansionEnabled;
147    }
148
149    public void closeQs() {
150        cancelAnimation();
151        setQsExpansion(mQsMinExpansionHeight);
152    }
153
154    public void openQs() {
155        cancelAnimation();
156        if (mQsExpansionEnabled) {
157            setQsExpansion(mQsMaxExpansionHeight);
158        }
159    }
160
161    @Override
162    public void fling(float vel, boolean always) {
163        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
164        if (gr != null) {
165            gr.tag(
166                "fling " + ((vel > 0) ? "open" : "closed"),
167                "notifications,v=" + vel);
168        }
169        super.fling(vel, always);
170    }
171
172    @Override
173    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
174        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
175            event.getText()
176                    .add(getContext().getString(R.string.accessibility_desc_notification_shade));
177            return true;
178        }
179
180        return super.dispatchPopulateAccessibilityEvent(event);
181    }
182
183    @Override
184    public boolean onInterceptTouchEvent(MotionEvent event) {
185        int pointerIndex = event.findPointerIndex(mTrackingPointer);
186        if (pointerIndex < 0) {
187            pointerIndex = 0;
188            mTrackingPointer = event.getPointerId(pointerIndex);
189        }
190        final float x = event.getX(pointerIndex);
191        final float y = event.getY(pointerIndex);
192
193        switch (event.getActionMasked()) {
194            case MotionEvent.ACTION_DOWN:
195                mInitialTouchY = y;
196                mInitialTouchX = x;
197                initVelocityTracker();
198                trackMovement(event);
199                if (shouldIntercept(mInitialTouchX, mInitialTouchY, 0)) {
200                    getParent().requestDisallowInterceptTouchEvent(true);
201                }
202                break;
203            case MotionEvent.ACTION_POINTER_UP:
204                final int upPointer = event.getPointerId(event.getActionIndex());
205                if (mTrackingPointer == upPointer) {
206                    // gesture is ongoing, find a new pointer to track
207                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
208                    mTrackingPointer = event.getPointerId(newIndex);
209                    mInitialTouchX = event.getX(newIndex);
210                    mInitialTouchY = event.getY(newIndex);
211                }
212                break;
213
214            case MotionEvent.ACTION_MOVE:
215                final float h = y - mInitialTouchY;
216                trackMovement(event);
217                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
218                        && shouldIntercept(mInitialTouchX, mInitialTouchY, h)) {
219                    onQsExpansionStarted();
220                    mInitialHeightOnTouch = mQsExpansionHeight;
221                    mInitialTouchY = y;
222                    mInitialTouchX = x;
223                    mTracking = true;
224                    return true;
225                }
226                break;
227        }
228        return !mQsExpanded && super.onInterceptTouchEvent(event);
229    }
230
231    @Override
232    public boolean onTouchEvent(MotionEvent event) {
233        // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
234        // implementation.
235        if (mTracking) {
236            int pointerIndex = event.findPointerIndex(mTrackingPointer);
237            if (pointerIndex < 0) {
238                pointerIndex = 0;
239                mTrackingPointer = event.getPointerId(pointerIndex);
240            }
241            final float y = event.getY(pointerIndex);
242            final float x = event.getX(pointerIndex);
243
244            switch (event.getActionMasked()) {
245                case MotionEvent.ACTION_DOWN:
246                    mTracking = true;
247                    mInitialTouchY = y;
248                    mInitialTouchX = x;
249                    onQsExpansionStarted();
250                    mInitialHeightOnTouch = mQsExpansionHeight;
251                    initVelocityTracker();
252                    trackMovement(event);
253                    break;
254
255                case MotionEvent.ACTION_POINTER_UP:
256                    final int upPointer = event.getPointerId(event.getActionIndex());
257                    if (mTrackingPointer == upPointer) {
258                        // gesture is ongoing, find a new pointer to track
259                        final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
260                        final float newY = event.getY(newIndex);
261                        final float newX = event.getX(newIndex);
262                        mTrackingPointer = event.getPointerId(newIndex);
263                        mInitialHeightOnTouch = mQsExpansionHeight;
264                        mInitialTouchY = newY;
265                        mInitialTouchX = newX;
266                    }
267                    break;
268
269                case MotionEvent.ACTION_MOVE:
270                    final float h = y - mInitialTouchY;
271                    setQsExpansion(h + mInitialHeightOnTouch);
272                    trackMovement(event);
273                    break;
274
275                case MotionEvent.ACTION_UP:
276                case MotionEvent.ACTION_CANCEL:
277                    mTracking = false;
278                    mTrackingPointer = -1;
279                    trackMovement(event);
280
281                    float vel = getCurrentVelocity();
282
283                    // TODO: Better logic whether we should expand or not.
284                    flingSettings(vel, vel > 0);
285
286                    if (mVelocityTracker != null) {
287                        mVelocityTracker.recycle();
288                        mVelocityTracker = null;
289                    }
290                    break;
291            }
292            return true;
293        }
294
295        // Consume touch events when QS are expanded.
296        return mQsExpanded || super.onTouchEvent(event);
297    }
298
299    private void onQsExpansionStarted() {
300        cancelAnimation();
301
302        // Reset scroll position and apply that position to the expanded height.
303        float height = mQsExpansionHeight - mScrollView.getScrollY();
304        mScrollView.scrollTo(0, 0);
305        setQsExpansion(height);
306    }
307
308    private void expandQs() {
309        mHeader.setExpanded(true);
310        mNotificationStackScroller.setEnabled(false);
311        mScrollView.setVisibility(View.VISIBLE);
312        mQsExpanded = true;
313    }
314
315    private void collapseQs() {
316        mHeader.setExpanded(false);
317        mNotificationStackScroller.setEnabled(true);
318        mScrollView.setVisibility(View.INVISIBLE);
319        mQsExpanded = false;
320    }
321
322    private void setQsExpansion(float height) {
323        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
324        if (height > mQsMinExpansionHeight && !mQsExpanded) {
325            expandQs();
326        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
327            collapseQs();
328        }
329        mQsExpansionHeight = height;
330        mHeader.setExpansion(height);
331        setQsTranslation(height);
332        setQsStackScrollerPadding(height);
333    }
334
335    private void setQsTranslation(float height) {
336        mQsContainer.setY(height - mQsContainer.getHeight());
337    }
338
339    private void setQsStackScrollerPadding(float height) {
340        float start = height - mScrollView.getScrollY() + mNotificationTopPadding;
341        float stackHeight = mNotificationStackScroller.getHeight() - start;
342        if (stackHeight <= mMinStackHeight) {
343            float overflow = mMinStackHeight - stackHeight;
344            stackHeight = mMinStackHeight;
345            start = mNotificationStackScroller.getHeight() - stackHeight;
346            mNotificationStackScroller.setTranslationY(overflow);
347            mNotificationTranslation = overflow + mScrollView.getScrollY();
348        } else {
349            mNotificationStackScroller.setTranslationY(0);
350            mNotificationTranslation = mScrollView.getScrollY();
351        }
352        mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false);
353    }
354
355    private int clampQsStackScrollerPadding(int desiredPadding) {
356        return Math.max(desiredPadding, mStackScrollerIntrinsicPadding);
357    }
358
359    private void trackMovement(MotionEvent event) {
360        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
361    }
362
363    private void initVelocityTracker() {
364        if (mVelocityTracker != null) {
365            mVelocityTracker.recycle();
366        }
367        mVelocityTracker = VelocityTracker.obtain();
368    }
369
370    private float getCurrentVelocity() {
371        if (mVelocityTracker == null) {
372            return 0;
373        }
374        mVelocityTracker.computeCurrentVelocity(1000);
375        return mVelocityTracker.getYVelocity();
376    }
377
378    private void cancelAnimation() {
379        if (mQsExpansionAnimator != null) {
380            mQsExpansionAnimator.cancel();
381        }
382    }
383    private void flingSettings(float vel, boolean expand) {
384
385        // TODO: Actually use velocity.
386
387        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
388        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
389        animator.setDuration(EXPANSION_ANIMATION_LENGTH);
390        animator.setInterpolator(mExpansionInterpolator);
391        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
392            @Override
393            public void onAnimationUpdate(ValueAnimator animation) {
394                setQsExpansion((Float) animation.getAnimatedValue());
395            }
396        });
397        animator.addListener(new AnimatorListenerAdapter() {
398            @Override
399            public void onAnimationEnd(Animator animation) {
400                mQsExpansionAnimator = null;
401            }
402        });
403        animator.start();
404        mQsExpansionAnimator = animator;
405    }
406
407    /**
408     * @return Whether we should intercept a gesture to open Quick Settings.
409     */
410    private boolean shouldIntercept(float x, float y, float yDiff) {
411        if (!mQsExpansionEnabled) {
412            return false;
413        }
414        View headerView = mStatusBar.getBarState() == StatusBarState.KEYGUARD && !mQsExpanded
415                ? mKeyguardStatusView
416                : mHeader;
417        boolean onHeader = x >= headerView.getLeft() && x <= headerView.getRight()
418                && y >= headerView.getTop() && y <= headerView.getBottom();
419        if (mQsExpanded) {
420            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
421        } else {
422            return onHeader;
423        }
424    }
425
426    @Override
427    protected boolean isScrolledToBottom() {
428        if (!isInSettings()) {
429            return mNotificationStackScroller.isScrolledToBottom();
430        }
431        return super.isScrolledToBottom();
432    }
433
434    @Override
435    protected int getMaxPanelHeight() {
436        if (!isInSettings()) {
437            int maxPanelHeight = super.getMaxPanelHeight();
438            int notificationMarginBottom = mStackScrollerContainer.getPaddingBottom();
439            int emptyBottomMargin = notificationMarginBottom
440                    + mNotificationStackScroller.getEmptyBottomMargin();
441            return maxPanelHeight - emptyBottomMargin;
442        }
443        return super.getMaxPanelHeight();
444    }
445
446    private boolean isInSettings() {
447        return mQsExpanded;
448    }
449
450    @Override
451    protected void onHeightUpdated(float expandedHeight) {
452        mNotificationStackScroller.setStackHeight(expandedHeight);
453    }
454
455    @Override
456    protected int getDesiredMeasureHeight() {
457        return mMaxPanelHeight;
458    }
459
460    @Override
461    protected void onExpandingStarted() {
462        super.onExpandingStarted();
463        mNotificationStackScroller.onExpansionStarted();
464    }
465
466    @Override
467    protected void onExpandingFinished() {
468        super.onExpandingFinished();
469        mNotificationStackScroller.onExpansionStopped();
470    }
471
472    @Override
473    public void onHeightChanged(ExpandableView view) {
474        requestPanelHeightUpdate();
475    }
476
477    @Override
478    public void onScrollChanged() {
479        if (mQsExpanded) {
480            mNotificationStackScroller.setTranslationY(
481                    mNotificationTranslation - mScrollView.getScrollY());
482        }
483    }
484
485    @Override
486    public void onClick(View v) {
487        if (v == mHeader.getBackgroundView()) {
488            onQsExpansionStarted();
489            if (mQsExpanded) {
490                flingSettings(0 /* vel */, false /* expand */);
491            } else if (mQsExpansionEnabled) {
492                flingSettings(0 /* vel */, true /* expand */);
493            }
494        }
495    }
496}
497