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