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