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