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