NotificationPanelView.java revision 2fbad7b6a724cf0a5b98b66fe639d58f5ab10af3
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.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.util.AttributeSet;
25import android.view.MotionEvent;
26import android.view.VelocityTracker;
27import android.view.View;
28import android.view.ViewGroup;
29import android.view.ViewTreeObserver;
30import android.view.accessibility.AccessibilityEvent;
31import android.view.animation.AnimationUtils;
32import android.view.animation.Interpolator;
33import android.widget.LinearLayout;
34
35import com.android.systemui.R;
36import com.android.systemui.statusbar.ExpandableView;
37import com.android.systemui.statusbar.FlingAnimationUtils;
38import com.android.systemui.statusbar.GestureRecorder;
39import com.android.systemui.statusbar.StatusBarState;
40import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
41import com.android.systemui.statusbar.stack.StackStateAnimator;
42
43public class NotificationPanelView extends PanelView implements
44        ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
45        View.OnClickListener {
46
47    PhoneStatusBar mStatusBar;
48    private StatusBarHeaderView mHeader;
49    private View mQsContainer;
50    private View mQsPanel;
51    private View mKeyguardStatusView;
52    private ObservableScrollView mScrollView;
53    private View mStackScrollerContainer;
54
55    private NotificationStackScrollLayout mNotificationStackScroller;
56    private int mNotificationTopPadding;
57    private boolean mAnimateNextTopPaddingChange;
58
59    private int mTrackingPointer;
60    private VelocityTracker mVelocityTracker;
61    private boolean mTracking;
62
63    /**
64     * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't
65     * intercepted yet.
66     */
67    private boolean mIntercepting;
68    private boolean mQsExpanded;
69    private boolean mKeyguardShowing;
70    private float mInitialHeightOnTouch;
71    private float mInitialTouchX;
72    private float mInitialTouchY;
73    private float mLastTouchX;
74    private float mLastTouchY;
75    private float mQsExpansionHeight;
76    private int mQsMinExpansionHeight;
77    private int mQsMaxExpansionHeight;
78    private int mMinStackHeight;
79    private int mQsPeekHeight;
80    private float mNotificationTranslation;
81    private int mStackScrollerIntrinsicPadding;
82    private boolean mQsExpansionEnabled = true;
83    private ValueAnimator mQsExpansionAnimator;
84    private FlingAnimationUtils mFlingAnimationUtils;
85    private int mStatusBarMinHeight;
86
87    private Interpolator mFastOutSlowInInterpolator;
88    private ObjectAnimator mClockAnimator;
89    private int mClockAnimationTarget = -1;
90    private int mTopPaddingAdjustment;
91    private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
92            new KeyguardClockPositionAlgorithm();
93    private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
94            new KeyguardClockPositionAlgorithm.Result();
95
96    public NotificationPanelView(Context context, AttributeSet attrs) {
97        super(context, attrs);
98    }
99
100    public void setStatusBar(PhoneStatusBar bar) {
101        if (mStatusBar != null) {
102            mStatusBar.setOnFlipRunnable(null);
103        }
104        mStatusBar = bar;
105        if (bar != null) {
106            mStatusBar.setOnFlipRunnable(new Runnable() {
107                @Override
108                public void run() {
109                    requestPanelHeightUpdate();
110                }
111            });
112        }
113    }
114
115    @Override
116    protected void onFinishInflate() {
117        super.onFinishInflate();
118        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
119        mHeader.getBackgroundView().setOnClickListener(this);
120        mHeader.setOverlayParent(this);
121        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
122        mStackScrollerContainer = findViewById(R.id.notification_container_parent);
123        mQsContainer = findViewById(R.id.quick_settings_container);
124        mQsPanel = findViewById(R.id.quick_settings_panel);
125        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
126        mScrollView.setListener(this);
127        mNotificationStackScroller = (NotificationStackScrollLayout)
128                findViewById(R.id.notification_stack_scroller);
129        mNotificationStackScroller.setOnHeightChangedListener(this);
130        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
131                android.R.interpolator.fast_out_slow_in);
132    }
133
134    @Override
135    protected void loadDimens() {
136        super.loadDimens();
137        mNotificationTopPadding = getResources().getDimensionPixelSize(
138                R.dimen.notifications_top_padding);
139        mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height);
140        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
141        mStatusBarMinHeight = getResources().getDimensionPixelSize(
142                com.android.internal.R.dimen.status_bar_height);
143        mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
144        mClockPositionAlgorithm.loadDimens(getResources());
145    }
146
147    @Override
148    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
149        super.onLayout(changed, left, top, right, bottom);
150
151        // Calculate quick setting heights.
152        mQsMinExpansionHeight = mHeader.getCollapsedHeight() + mQsPeekHeight;
153        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
154        if (!mQsExpanded) {
155            setQsExpansion(mQsMinExpansionHeight);
156            positionClockAndNotifications();
157            mNotificationStackScroller.setStackHeight(getExpandedHeight());
158        }
159    }
160
161    /**
162     * Positions the clock and notifications dynamically depending on how many notifications are
163     * showing.
164     */
165    private void positionClockAndNotifications() {
166        boolean animateClock = mNotificationStackScroller.isAddOrRemoveAnimationPending();
167        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
168            mStackScrollerIntrinsicPadding = mHeader.getBottom() + mQsPeekHeight
169                    + mNotificationTopPadding;
170            mTopPaddingAdjustment = 0;
171        } else {
172            mClockPositionAlgorithm.setup(
173                    mStatusBar.getMaxKeyguardNotifications(),
174                    getMaxPanelHeight(),
175                    getExpandedHeight(),
176                    mNotificationStackScroller.getNotGoneChildCount(),
177                    getHeight(),
178                    mKeyguardStatusView.getHeight());
179            mClockPositionAlgorithm.run(mClockPositionResult);
180            if (animateClock || mClockAnimator != null) {
181                startClockAnimation(mClockPositionResult.clockY);
182            } else {
183                mKeyguardStatusView.setY(mClockPositionResult.clockY);
184            }
185            applyClockAlpha(mClockPositionResult.clockAlpha);
186            mStackScrollerIntrinsicPadding = mClockPositionResult.stackScrollerPadding;
187            mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
188        }
189        mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding,
190                mAnimateNextTopPaddingChange || animateClock);
191        mAnimateNextTopPaddingChange = false;
192    }
193
194    private void startClockAnimation(int y) {
195        if (mClockAnimationTarget == y) {
196            return;
197        }
198        mClockAnimationTarget = y;
199        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
200            @Override
201            public boolean onPreDraw() {
202                getViewTreeObserver().removeOnPreDrawListener(this);
203                if (mClockAnimator != null) {
204                    mClockAnimator.removeAllListeners();
205                    mClockAnimator.cancel();
206                }
207                mClockAnimator =
208                        ObjectAnimator.ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget);
209                mClockAnimator.setInterpolator(mFastOutSlowInInterpolator);
210                mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
211                mClockAnimator.addListener(new AnimatorListenerAdapter() {
212                    @Override
213                    public void onAnimationEnd(Animator animation) {
214                        mClockAnimator = null;
215                        mClockAnimationTarget = -1;
216                    }
217                });
218                mClockAnimator.start();
219                return true;
220            }
221        });
222    }
223
224    private void applyClockAlpha(float alpha) {
225        if (alpha != 1.0f) {
226            mKeyguardStatusView.setLayerType(LAYER_TYPE_HARDWARE, null);
227        } else {
228            mKeyguardStatusView.setLayerType(LAYER_TYPE_NONE, null);
229        }
230        mKeyguardStatusView.setAlpha(alpha);
231    }
232
233    public void animateToFullShade() {
234        mAnimateNextTopPaddingChange = true;
235        mNotificationStackScroller.goToFullShade();
236        requestLayout();
237    }
238
239    /**
240     * @return Whether Quick Settings are currently expanded.
241     */
242    public boolean isQsExpanded() {
243        return mQsExpanded;
244    }
245
246    public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
247        mQsExpansionEnabled = qsExpansionEnabled;
248    }
249
250    public void closeQs() {
251        cancelAnimation();
252        setQsExpansion(mQsMinExpansionHeight);
253    }
254
255    public void openQs() {
256        cancelAnimation();
257        if (mQsExpansionEnabled) {
258            setQsExpansion(mQsMaxExpansionHeight);
259        }
260    }
261
262    @Override
263    public void fling(float vel, boolean always) {
264        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
265        if (gr != null) {
266            gr.tag(
267                "fling " + ((vel > 0) ? "open" : "closed"),
268                "notifications,v=" + vel);
269        }
270        super.fling(vel, always);
271    }
272
273    @Override
274    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
275        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
276            event.getText()
277                    .add(getContext().getString(R.string.accessibility_desc_notification_shade));
278            return true;
279        }
280
281        return super.dispatchPopulateAccessibilityEvent(event);
282    }
283
284    @Override
285    public boolean onInterceptTouchEvent(MotionEvent event) {
286        int pointerIndex = event.findPointerIndex(mTrackingPointer);
287        if (pointerIndex < 0) {
288            pointerIndex = 0;
289            mTrackingPointer = event.getPointerId(pointerIndex);
290        }
291        final float x = event.getX(pointerIndex);
292        final float y = event.getY(pointerIndex);
293
294        switch (event.getActionMasked()) {
295            case MotionEvent.ACTION_DOWN:
296                mIntercepting = true;
297                mInitialTouchY = y;
298                mInitialTouchX = x;
299                initVelocityTracker();
300                trackMovement(event);
301                if (shouldIntercept(mInitialTouchX, mInitialTouchY, 0)) {
302                    getParent().requestDisallowInterceptTouchEvent(true);
303                }
304                break;
305            case MotionEvent.ACTION_POINTER_UP:
306                final int upPointer = event.getPointerId(event.getActionIndex());
307                if (mTrackingPointer == upPointer) {
308                    // gesture is ongoing, find a new pointer to track
309                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
310                    mTrackingPointer = event.getPointerId(newIndex);
311                    mInitialTouchX = event.getX(newIndex);
312                    mInitialTouchY = event.getY(newIndex);
313                }
314                break;
315
316            case MotionEvent.ACTION_MOVE:
317                final float h = y - mInitialTouchY;
318                trackMovement(event);
319                if (mTracking) {
320
321                    // Already tracking because onOverscrolled was called. We need to update here
322                    // so we don't stop for a frame until the next touch event gets handled in
323                    // onTouchEvent.
324                    setQsExpansion(h + mInitialHeightOnTouch);
325                    trackMovement(event);
326                    mIntercepting = false;
327                    return true;
328                }
329                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
330                        && shouldIntercept(mInitialTouchX, mInitialTouchY, h)) {
331                    onQsExpansionStarted();
332                    mInitialHeightOnTouch = mQsExpansionHeight;
333                    mInitialTouchY = y;
334                    mInitialTouchX = x;
335                    mTracking = true;
336                    mIntercepting = false;
337                    return true;
338                }
339                break;
340
341            case MotionEvent.ACTION_CANCEL:
342            case MotionEvent.ACTION_UP:
343                trackMovement(event);
344                if (mTracking) {
345                    flingWithCurrentVelocity();
346                    mTracking = false;
347                }
348                mIntercepting = false;
349                break;
350        }
351        return !mQsExpanded && super.onInterceptTouchEvent(event);
352    }
353
354    @Override
355    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
356
357        // Block request when interacting with the scroll view so we can still intercept the
358        // scrolling when QS is expanded.
359        if (mScrollView.isDispatchingTouchEvent()) {
360            return;
361        }
362        super.requestDisallowInterceptTouchEvent(disallowIntercept);
363    }
364
365    private void flingWithCurrentVelocity() {
366        float vel = getCurrentVelocity();
367
368        // TODO: Better logic whether we should expand or not.
369        flingSettings(vel, vel > 0);
370    }
371
372    @Override
373    public boolean onTouchEvent(MotionEvent event) {
374        // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
375        // implementation.
376        if (mTracking) {
377            int pointerIndex = event.findPointerIndex(mTrackingPointer);
378            if (pointerIndex < 0) {
379                pointerIndex = 0;
380                mTrackingPointer = event.getPointerId(pointerIndex);
381            }
382            final float y = event.getY(pointerIndex);
383            final float x = event.getX(pointerIndex);
384
385            switch (event.getActionMasked()) {
386                case MotionEvent.ACTION_DOWN:
387                    mTracking = true;
388                    mInitialTouchY = y;
389                    mInitialTouchX = x;
390                    onQsExpansionStarted();
391                    mInitialHeightOnTouch = mQsExpansionHeight;
392                    initVelocityTracker();
393                    trackMovement(event);
394                    break;
395
396                case MotionEvent.ACTION_POINTER_UP:
397                    final int upPointer = event.getPointerId(event.getActionIndex());
398                    if (mTrackingPointer == upPointer) {
399                        // gesture is ongoing, find a new pointer to track
400                        final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
401                        final float newY = event.getY(newIndex);
402                        final float newX = event.getX(newIndex);
403                        mTrackingPointer = event.getPointerId(newIndex);
404                        mInitialHeightOnTouch = mQsExpansionHeight;
405                        mInitialTouchY = newY;
406                        mInitialTouchX = newX;
407                    }
408                    break;
409
410                case MotionEvent.ACTION_MOVE:
411                    final float h = y - mInitialTouchY;
412                    setQsExpansion(h + mInitialHeightOnTouch);
413                    trackMovement(event);
414                    break;
415
416                case MotionEvent.ACTION_UP:
417                case MotionEvent.ACTION_CANCEL:
418                    mTracking = false;
419                    mTrackingPointer = -1;
420                    trackMovement(event);
421                    flingWithCurrentVelocity();
422                    if (mVelocityTracker != null) {
423                        mVelocityTracker.recycle();
424                        mVelocityTracker = null;
425                    }
426                    break;
427            }
428            return true;
429        }
430
431        // Consume touch events when QS are expanded.
432        return mQsExpanded || super.onTouchEvent(event);
433    }
434
435    @Override
436    public void onOverscrolled(int amount) {
437        if (mIntercepting) {
438            onQsExpansionStarted(amount);
439            mInitialHeightOnTouch = mQsExpansionHeight;
440            mInitialTouchY = mLastTouchY;
441            mInitialTouchX = mLastTouchX;
442            mTracking = true;
443        }
444    }
445
446    private void onQsExpansionStarted() {
447        onQsExpansionStarted(0);
448    }
449
450    private void onQsExpansionStarted(int overscrollAmount) {
451        cancelAnimation();
452
453        // Reset scroll position and apply that position to the expanded height.
454        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
455        mScrollView.scrollTo(0, 0);
456        setQsExpansion(height);
457    }
458
459    private void setQsExpanded(boolean expanded) {
460        boolean changed = mQsExpanded != expanded;
461        if (changed) {
462            mQsExpanded = expanded;
463            updateQsState();
464        }
465    }
466
467    public void setKeyguardShowing(boolean keyguardShowing) {
468        mKeyguardShowing = keyguardShowing;
469        updateQsState();
470    }
471
472    private void updateQsState() {
473        mHeader.setExpanded(mQsExpanded);
474        mNotificationStackScroller.setEnabled(!mQsExpanded);
475        mQsPanel.setVisibility(mQsExpanded ? View.VISIBLE : View.INVISIBLE);
476        mQsContainer.setVisibility(mKeyguardShowing && !mQsExpanded
477                ? View.INVISIBLE
478                : View.VISIBLE);
479        mScrollView.setTouchEnabled(mQsExpanded);
480    }
481
482    private void setQsExpansion(float height) {
483        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
484        if (height > mQsMinExpansionHeight && !mQsExpanded) {
485            setQsExpanded(true);
486        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
487            setQsExpanded(false);
488        }
489        mQsExpansionHeight = height;
490        mHeader.setExpansion(height - mQsPeekHeight);
491        setQsTranslation(height);
492        setQsStackScrollerPadding(height);
493        mStatusBar.userActivity();
494    }
495
496    private void setQsTranslation(float height) {
497        mQsContainer.setY(height - mQsContainer.getHeight());
498    }
499
500    private void setQsStackScrollerPadding(float height) {
501        float start = height - mScrollView.getScrollY() + mNotificationTopPadding;
502        float stackHeight = mNotificationStackScroller.getHeight() - start;
503        if (stackHeight <= mMinStackHeight) {
504            float overflow = mMinStackHeight - stackHeight;
505            stackHeight = mMinStackHeight;
506            start = mNotificationStackScroller.getHeight() - stackHeight;
507            mNotificationStackScroller.setTranslationY(overflow);
508            mNotificationTranslation = overflow + mScrollView.getScrollY();
509        } else {
510            mNotificationStackScroller.setTranslationY(0);
511            mNotificationTranslation = mScrollView.getScrollY();
512        }
513        mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false);
514    }
515
516    private int clampQsStackScrollerPadding(int desiredPadding) {
517        return Math.max(desiredPadding, mStackScrollerIntrinsicPadding);
518    }
519
520    private void trackMovement(MotionEvent event) {
521        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
522        mLastTouchX = event.getX();
523        mLastTouchY = event.getY();
524    }
525
526    private void initVelocityTracker() {
527        if (mVelocityTracker != null) {
528            mVelocityTracker.recycle();
529        }
530        mVelocityTracker = VelocityTracker.obtain();
531    }
532
533    private float getCurrentVelocity() {
534        if (mVelocityTracker == null) {
535            return 0;
536        }
537        mVelocityTracker.computeCurrentVelocity(1000);
538        return mVelocityTracker.getYVelocity();
539    }
540
541    private void cancelAnimation() {
542        if (mQsExpansionAnimator != null) {
543            mQsExpansionAnimator.cancel();
544        }
545    }
546    private void flingSettings(float vel, boolean expand) {
547        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
548        if (target == mQsExpansionHeight) {
549            return;
550        }
551        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
552        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
553        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
554            @Override
555            public void onAnimationUpdate(ValueAnimator animation) {
556                setQsExpansion((Float) animation.getAnimatedValue());
557            }
558        });
559        animator.addListener(new AnimatorListenerAdapter() {
560            @Override
561            public void onAnimationEnd(Animator animation) {
562                mQsExpansionAnimator = null;
563            }
564        });
565        animator.start();
566        mQsExpansionAnimator = animator;
567    }
568
569    /**
570     * @return Whether we should intercept a gesture to open Quick Settings.
571     */
572    private boolean shouldIntercept(float x, float y, float yDiff) {
573        if (!mQsExpansionEnabled) {
574            return false;
575        }
576        boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight()
577                && y >= mHeader.getTop() && y <= mHeader.getBottom();
578        if (mQsExpanded) {
579            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
580        } else {
581            return onHeader;
582        }
583    }
584
585    @Override
586    public void setVisibility(int visibility) {
587        int oldVisibility = getVisibility();
588        super.setVisibility(visibility);
589        if (visibility != oldVisibility) {
590            reparentStatusIcons(visibility == VISIBLE);
591        }
592    }
593
594    /**
595     * When the notification panel gets expanded, we need to move the status icons in the header
596     * card.
597     */
598    private void reparentStatusIcons(boolean toHeader) {
599        if (mStatusBar == null) {
600            return;
601        }
602        LinearLayout systemIcons = mStatusBar.getSystemIcons();
603        if (systemIcons.getParent() != null) {
604            ((ViewGroup) systemIcons.getParent()).removeView(systemIcons);
605        }
606        if (toHeader) {
607            mHeader.attachSystemIcons(systemIcons);
608        } else {
609            mHeader.onSystemIconsDetached();
610            mStatusBar.reattachSystemIcons();
611        }
612    }
613
614    @Override
615    protected boolean isScrolledToBottom() {
616        if (!isInSettings()) {
617            return mNotificationStackScroller.isScrolledToBottom();
618        }
619        return super.isScrolledToBottom();
620    }
621
622    @Override
623    protected int getMaxPanelHeight() {
624        // TODO: Figure out transition for collapsing when QS is open, adjust height here.
625        int maxPanelHeight = super.getMaxPanelHeight();
626        int emptyBottomMargin = mStackScrollerContainer.getHeight()
627                - mNotificationStackScroller.getHeight()
628                + mNotificationStackScroller.getEmptyBottomMargin();
629        int maxHeight = maxPanelHeight - emptyBottomMargin - mTopPaddingAdjustment;
630        maxHeight = Math.max(maxHeight, mStatusBarMinHeight);
631        return maxHeight;
632    }
633
634    private boolean isInSettings() {
635        return mQsExpanded;
636    }
637
638    @Override
639    protected void onHeightUpdated(float expandedHeight) {
640        if (!mQsExpanded) {
641            positionClockAndNotifications();
642        }
643        mNotificationStackScroller.setStackHeight(expandedHeight);
644    }
645
646    @Override
647    protected void onExpandingStarted() {
648        super.onExpandingStarted();
649        mNotificationStackScroller.onExpansionStarted();
650    }
651
652    @Override
653    protected void onExpandingFinished() {
654        super.onExpandingFinished();
655        mNotificationStackScroller.onExpansionStopped();
656    }
657
658    @Override
659    protected void onOverExpansionChanged(float overExpansion) {
660        float currentOverScroll = mNotificationStackScroller.getCurrentOverScrolledPixels(true);
661        mNotificationStackScroller.setOverScrolledPixels(currentOverScroll + overExpansion
662                        - mOverExpansion, true /* onTop */, false /* animate */);
663        super.onOverExpansionChanged(overExpansion);
664    }
665
666    @Override
667    protected void onTrackingStopped(boolean expand) {
668        super.onTrackingStopped(expand);
669        mOverExpansion = 0.0f;
670        mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */,
671                true /* animate */);
672    }
673
674
675    @Override
676    public void onHeightChanged(ExpandableView view) {
677        requestPanelHeightUpdate();
678    }
679
680    @Override
681    public void onScrollChanged() {
682        if (mQsExpanded) {
683            mNotificationStackScroller.setTranslationY(
684                    mNotificationTranslation - mScrollView.getScrollY());
685        }
686    }
687
688    @Override
689    public void onClick(View v) {
690        if (v == mHeader.getBackgroundView()) {
691            onQsExpansionStarted();
692            if (mQsExpanded) {
693                flingSettings(0 /* vel */, false /* expand */);
694            } else if (mQsExpansionEnabled) {
695                flingSettings(0 /* vel */, true /* expand */);
696            }
697        }
698    }
699}
700