NotificationPanelView.java revision 9cd731a013cca45807b2ae1ed19cecc53311a5c6
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.PropertyValuesHolder;
23import android.animation.ValueAnimator;
24import android.content.Context;
25import android.util.AttributeSet;
26import android.view.MotionEvent;
27import android.view.VelocityTracker;
28import android.view.View;
29import android.view.ViewGroup;
30import android.view.ViewTreeObserver;
31import android.view.accessibility.AccessibilityEvent;
32import android.view.animation.AnimationUtils;
33import android.view.animation.Interpolator;
34import android.widget.LinearLayout;
35
36import com.android.systemui.R;
37import com.android.systemui.statusbar.ExpandableView;
38import com.android.systemui.statusbar.FlingAnimationUtils;
39import com.android.systemui.statusbar.GestureRecorder;
40import com.android.systemui.statusbar.StatusBarState;
41import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
42import com.android.systemui.statusbar.stack.StackStateAnimator;
43
44public class NotificationPanelView extends PanelView implements
45        ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
46        View.OnClickListener {
47
48    PhoneStatusBar mStatusBar;
49    private StatusBarHeaderView mHeader;
50    private View mQsContainer;
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 float mInitialHeightOnTouch;
70    private float mInitialTouchX;
71    private float mInitialTouchY;
72    private float mLastTouchX;
73    private float mLastTouchY;
74    private float mQsExpansionHeight;
75    private int mQsMinExpansionHeight;
76    private int mQsMaxExpansionHeight;
77    private int mMinStackHeight;
78    private float mNotificationTranslation;
79    private int mStackScrollerIntrinsicPadding;
80    private boolean mQsExpansionEnabled = true;
81    private ValueAnimator mQsExpansionAnimator;
82    private FlingAnimationUtils mFlingAnimationUtils;
83    private int mStatusBarMinHeight;
84
85    private int mClockNotificationsMarginMin;
86    private int mClockNotificationsMarginMax;
87    private float mClockYFractionMin;
88    private float mClockYFractionMax;
89    private Interpolator mFastOutSlowInInterpolator;
90    private ObjectAnimator mClockAnimator;
91    private int mClockAnimationTarget = -1;
92
93    /**
94     * The number (fractional) of notifications the "more" card counts when calculating how many
95     * notifications are currently visible for the y positioning of the clock.
96     */
97    private float mMoreCardNotificationAmount;
98
99    public NotificationPanelView(Context context, AttributeSet attrs) {
100        super(context, attrs);
101    }
102
103    public void setStatusBar(PhoneStatusBar bar) {
104        if (mStatusBar != null) {
105            mStatusBar.setOnFlipRunnable(null);
106        }
107        mStatusBar = bar;
108        if (bar != null) {
109            mStatusBar.setOnFlipRunnable(new Runnable() {
110                @Override
111                public void run() {
112                    requestPanelHeightUpdate();
113                }
114            });
115        }
116    }
117
118    @Override
119    protected void onFinishInflate() {
120        super.onFinishInflate();
121        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
122        mHeader.getBackgroundView().setOnClickListener(this);
123        mHeader.setOverlayParent(this);
124        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
125        mStackScrollerContainer = findViewById(R.id.notification_container_parent);
126        mQsContainer = findViewById(R.id.quick_settings_container);
127        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
128        mScrollView.setListener(this);
129        mNotificationStackScroller = (NotificationStackScrollLayout)
130                findViewById(R.id.notification_stack_scroller);
131        mNotificationStackScroller.setOnHeightChangedListener(this);
132        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
133                android.R.interpolator.fast_out_slow_in);
134    }
135
136    @Override
137    protected void loadDimens() {
138        super.loadDimens();
139        mNotificationTopPadding = getResources().getDimensionPixelSize(
140                R.dimen.notifications_top_padding);
141        mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height);
142        mClockNotificationsMarginMin = getResources().getDimensionPixelSize(
143                R.dimen.keyguard_clock_notifications_margin_min);
144        mClockNotificationsMarginMax = getResources().getDimensionPixelSize(
145                R.dimen.keyguard_clock_notifications_margin_max);
146        mClockYFractionMin =
147                getResources().getFraction(R.fraction.keyguard_clock_y_fraction_min, 1, 1);
148        mClockYFractionMax =
149                getResources().getFraction(R.fraction.keyguard_clock_y_fraction_max, 1, 1);
150        mMoreCardNotificationAmount =
151                (float) getResources().getDimensionPixelSize(R.dimen.notification_summary_height) /
152                        getResources().getDimensionPixelSize(R.dimen.notification_min_height);
153        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
154        mStatusBarMinHeight = getResources().getDimensionPixelSize(
155                com.android.internal.R.dimen.status_bar_height);
156    }
157
158    @Override
159    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
160        super.onLayout(changed, left, top, right, bottom);
161        if (!mQsExpanded) {
162            positionClockAndNotifications();
163        }
164
165        // Calculate quick setting heights.
166        mQsMinExpansionHeight = mHeader.getCollapsedHeight();
167        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
168        if (mQsExpansionHeight == 0) {
169            mQsExpansionHeight = mQsMinExpansionHeight;
170        }
171    }
172
173    /**
174     * Positions the clock and notifications dynamically depending on how many notifications are
175     * showing.
176     */
177    private void positionClockAndNotifications() {
178        boolean animateClock = mNotificationStackScroller.isAddOrRemoveAnimationPending();
179        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
180            mStackScrollerIntrinsicPadding = mHeader.getBottom() + mNotificationTopPadding;
181        } else {
182            int notificationCount = mNotificationStackScroller.getNotGoneChildCount();
183            int y = getClockY(notificationCount) - mKeyguardStatusView.getHeight()/2;
184            int padding = getClockNotificationsPadding(notificationCount);
185            if (animateClock || mClockAnimator != null) {
186                startClockAnimation(y);
187            } else {
188                mKeyguardStatusView.setY(y);
189            }
190            mStackScrollerIntrinsicPadding = y + mKeyguardStatusView.getHeight() + padding;
191        }
192        mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding,
193                mAnimateNextTopPaddingChange || animateClock);
194        mAnimateNextTopPaddingChange = false;
195    }
196
197    private void startClockAnimation(int y) {
198        if (mClockAnimationTarget == y) {
199            return;
200        }
201        mClockAnimationTarget = y;
202        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
203            @Override
204            public boolean onPreDraw() {
205                getViewTreeObserver().removeOnPreDrawListener(this);
206                if (mClockAnimator != null) {
207                    mClockAnimator.removeAllListeners();
208                    mClockAnimator.cancel();
209                }
210                mClockAnimator =
211                        ObjectAnimator.ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget);
212                mClockAnimator.setInterpolator(mFastOutSlowInInterpolator);
213                mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
214                mClockAnimator.addListener(new AnimatorListenerAdapter() {
215                    @Override
216                    public void onAnimationEnd(Animator animation) {
217                        mClockAnimator = null;
218                        mClockAnimationTarget = -1;
219                    }
220                });
221                StackStateAnimator.startInstantly(mClockAnimator);
222                return true;
223            }
224        });
225    }
226
227    private int getClockNotificationsPadding(int notificationCount) {
228        float t = notificationCount
229                / (mStatusBar.getMaxKeyguardNotifications() + mMoreCardNotificationAmount);
230        t = Math.min(t, 1.0f);
231        return (int) (t * mClockNotificationsMarginMin + (1 - t) * mClockNotificationsMarginMax);
232    }
233
234    private float getClockYFraction(int notificationCount) {
235        float t = notificationCount
236                / (mStatusBar.getMaxKeyguardNotifications() + mMoreCardNotificationAmount);
237        t = Math.min(t, 1.0f);
238        return (1 - t) * mClockYFractionMax + t * mClockYFractionMin;
239    }
240
241    private int getClockY(int notificationCount) {
242        return (int) (getClockYFraction(notificationCount) * getHeight());
243    }
244
245    public void animateToFullShade() {
246        mAnimateNextTopPaddingChange = true;
247        mNotificationStackScroller.goToFullShade();
248        requestLayout();
249    }
250
251    /**
252     * @return Whether Quick Settings are currently expanded.
253     */
254    public boolean isQsExpanded() {
255        return mQsExpanded;
256    }
257
258    public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
259        mQsExpansionEnabled = qsExpansionEnabled;
260    }
261
262    public void closeQs() {
263        cancelAnimation();
264        setQsExpansion(mQsMinExpansionHeight);
265    }
266
267    public void openQs() {
268        cancelAnimation();
269        if (mQsExpansionEnabled) {
270            setQsExpansion(mQsMaxExpansionHeight);
271        }
272    }
273
274    @Override
275    public void fling(float vel, boolean always) {
276        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
277        if (gr != null) {
278            gr.tag(
279                "fling " + ((vel > 0) ? "open" : "closed"),
280                "notifications,v=" + vel);
281        }
282        super.fling(vel, always);
283    }
284
285    @Override
286    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
287        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
288            event.getText()
289                    .add(getContext().getString(R.string.accessibility_desc_notification_shade));
290            return true;
291        }
292
293        return super.dispatchPopulateAccessibilityEvent(event);
294    }
295
296    @Override
297    public boolean onInterceptTouchEvent(MotionEvent event) {
298        int pointerIndex = event.findPointerIndex(mTrackingPointer);
299        if (pointerIndex < 0) {
300            pointerIndex = 0;
301            mTrackingPointer = event.getPointerId(pointerIndex);
302        }
303        final float x = event.getX(pointerIndex);
304        final float y = event.getY(pointerIndex);
305
306        switch (event.getActionMasked()) {
307            case MotionEvent.ACTION_DOWN:
308                mIntercepting = true;
309                mInitialTouchY = y;
310                mInitialTouchX = x;
311                initVelocityTracker();
312                trackMovement(event);
313                if (shouldIntercept(mInitialTouchX, mInitialTouchY, 0)) {
314                    getParent().requestDisallowInterceptTouchEvent(true);
315                }
316                break;
317            case MotionEvent.ACTION_POINTER_UP:
318                final int upPointer = event.getPointerId(event.getActionIndex());
319                if (mTrackingPointer == upPointer) {
320                    // gesture is ongoing, find a new pointer to track
321                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
322                    mTrackingPointer = event.getPointerId(newIndex);
323                    mInitialTouchX = event.getX(newIndex);
324                    mInitialTouchY = event.getY(newIndex);
325                }
326                break;
327
328            case MotionEvent.ACTION_MOVE:
329                final float h = y - mInitialTouchY;
330                trackMovement(event);
331                if (mTracking) {
332
333                    // Already tracking because onOverscrolled was called. We need to update here
334                    // so we don't stop for a frame until the next touch event gets handled in
335                    // onTouchEvent.
336                    setQsExpansion(h + mInitialHeightOnTouch);
337                    trackMovement(event);
338                    mIntercepting = false;
339                    return true;
340                }
341                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
342                        && shouldIntercept(mInitialTouchX, mInitialTouchY, h)) {
343                    onQsExpansionStarted();
344                    mInitialHeightOnTouch = mQsExpansionHeight;
345                    mInitialTouchY = y;
346                    mInitialTouchX = x;
347                    mTracking = true;
348                    mIntercepting = false;
349                    return true;
350                }
351                break;
352
353            case MotionEvent.ACTION_CANCEL:
354            case MotionEvent.ACTION_UP:
355                trackMovement(event);
356                if (mTracking) {
357                    flingWithCurrentVelocity();
358                    mTracking = false;
359                }
360                mIntercepting = false;
361                break;
362        }
363        return !mQsExpanded && super.onInterceptTouchEvent(event);
364    }
365
366    @Override
367    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
368
369        // Block request so we can still intercept the scrolling when QS is expanded.
370        if (!mQsExpanded) {
371            super.requestDisallowInterceptTouchEvent(disallowIntercept);
372        }
373    }
374
375    private void flingWithCurrentVelocity() {
376        float vel = getCurrentVelocity();
377
378        // TODO: Better logic whether we should expand or not.
379        flingSettings(vel, vel > 0);
380    }
381
382    @Override
383    public boolean onTouchEvent(MotionEvent event) {
384        // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
385        // implementation.
386        if (mTracking) {
387            int pointerIndex = event.findPointerIndex(mTrackingPointer);
388            if (pointerIndex < 0) {
389                pointerIndex = 0;
390                mTrackingPointer = event.getPointerId(pointerIndex);
391            }
392            final float y = event.getY(pointerIndex);
393            final float x = event.getX(pointerIndex);
394
395            switch (event.getActionMasked()) {
396                case MotionEvent.ACTION_DOWN:
397                    mTracking = true;
398                    mInitialTouchY = y;
399                    mInitialTouchX = x;
400                    onQsExpansionStarted();
401                    mInitialHeightOnTouch = mQsExpansionHeight;
402                    initVelocityTracker();
403                    trackMovement(event);
404                    break;
405
406                case MotionEvent.ACTION_POINTER_UP:
407                    final int upPointer = event.getPointerId(event.getActionIndex());
408                    if (mTrackingPointer == upPointer) {
409                        // gesture is ongoing, find a new pointer to track
410                        final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
411                        final float newY = event.getY(newIndex);
412                        final float newX = event.getX(newIndex);
413                        mTrackingPointer = event.getPointerId(newIndex);
414                        mInitialHeightOnTouch = mQsExpansionHeight;
415                        mInitialTouchY = newY;
416                        mInitialTouchX = newX;
417                    }
418                    break;
419
420                case MotionEvent.ACTION_MOVE:
421                    final float h = y - mInitialTouchY;
422                    setQsExpansion(h + mInitialHeightOnTouch);
423                    trackMovement(event);
424                    break;
425
426                case MotionEvent.ACTION_UP:
427                case MotionEvent.ACTION_CANCEL:
428                    mTracking = false;
429                    mTrackingPointer = -1;
430                    trackMovement(event);
431                    flingWithCurrentVelocity();
432                    if (mVelocityTracker != null) {
433                        mVelocityTracker.recycle();
434                        mVelocityTracker = null;
435                    }
436                    break;
437            }
438            return true;
439        }
440
441        // Consume touch events when QS are expanded.
442        return mQsExpanded || super.onTouchEvent(event);
443    }
444
445    @Override
446    public void onOverscrolled(int amount) {
447        if (mIntercepting) {
448            onQsExpansionStarted(amount);
449            mInitialHeightOnTouch = mQsExpansionHeight;
450            mInitialTouchY = mLastTouchY;
451            mInitialTouchX = mLastTouchX;
452            mTracking = true;
453        }
454    }
455
456    private void onQsExpansionStarted() {
457        onQsExpansionStarted(0);
458    }
459
460    private void onQsExpansionStarted(int overscrollAmount) {
461        cancelAnimation();
462
463        // Reset scroll position and apply that position to the expanded height.
464        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
465        mScrollView.scrollTo(0, 0);
466        setQsExpansion(height);
467    }
468
469    private void expandQs() {
470        mHeader.setExpanded(true);
471        mNotificationStackScroller.setEnabled(false);
472        mScrollView.setVisibility(View.VISIBLE);
473        mQsExpanded = true;
474    }
475
476    private void collapseQs() {
477        mHeader.setExpanded(false);
478        mNotificationStackScroller.setEnabled(true);
479        mScrollView.setVisibility(View.INVISIBLE);
480        mQsExpanded = false;
481    }
482
483    private void setQsExpansion(float height) {
484        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
485        if (height > mQsMinExpansionHeight && !mQsExpanded) {
486            expandQs();
487        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
488            collapseQs();
489        }
490        mQsExpansionHeight = height;
491        mHeader.setExpansion(height);
492        setQsTranslation(height);
493        setQsStackScrollerPadding(height);
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;
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        mNotificationStackScroller.setStackHeight(expandedHeight);
641    }
642
643    @Override
644    protected void onExpandingStarted() {
645        super.onExpandingStarted();
646        mNotificationStackScroller.onExpansionStarted();
647    }
648
649    @Override
650    protected void onExpandingFinished() {
651        super.onExpandingFinished();
652        mNotificationStackScroller.onExpansionStopped();
653    }
654
655    @Override
656    public void onHeightChanged(ExpandableView view) {
657        requestPanelHeightUpdate();
658    }
659
660    @Override
661    public void onScrollChanged() {
662        if (mQsExpanded) {
663            mNotificationStackScroller.setTranslationY(
664                    mNotificationTranslation - mScrollView.getScrollY());
665        }
666    }
667
668    @Override
669    public void onClick(View v) {
670        if (v == mHeader.getBackgroundView()) {
671            onQsExpansionStarted();
672            if (mQsExpanded) {
673                flingSettings(0 /* vel */, false /* expand */);
674            } else if (mQsExpansionEnabled) {
675                flingSettings(0 /* vel */, true /* expand */);
676            }
677        }
678    }
679}
680