NotificationPanelView.java revision 4c6969a512cd70831249ec1d07691f16fe5465f5
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    public void resetViews() {
264        mBlockTouches = false;
265        mPageSwiper.reset();
266        closeQs();
267    }
268
269    public void closeQs() {
270        cancelAnimation();
271        setQsExpansion(mQsMinExpansionHeight);
272    }
273
274    public void openQs() {
275        cancelAnimation();
276        if (mQsExpansionEnabled) {
277            setQsExpansion(mQsMaxExpansionHeight);
278        }
279    }
280
281    @Override
282    public void fling(float vel, boolean always) {
283        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
284        if (gr != null) {
285            gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
286        }
287        super.fling(vel, always);
288    }
289
290    @Override
291    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
292        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
293            event.getText()
294                    .add(getContext().getString(R.string.accessibility_desc_notification_shade));
295            return true;
296        }
297
298        return super.dispatchPopulateAccessibilityEvent(event);
299    }
300
301    @Override
302    public boolean onInterceptTouchEvent(MotionEvent event) {
303        if (mBlockTouches) {
304            return false;
305        }
306        int pointerIndex = event.findPointerIndex(mTrackingPointer);
307        if (pointerIndex < 0) {
308            pointerIndex = 0;
309            mTrackingPointer = event.getPointerId(pointerIndex);
310        }
311        final float x = event.getX(pointerIndex);
312        final float y = event.getY(pointerIndex);
313
314        switch (event.getActionMasked()) {
315            case MotionEvent.ACTION_DOWN:
316                mIntercepting = true;
317                mInitialTouchY = y;
318                mInitialTouchX = x;
319                initVelocityTracker();
320                trackMovement(event);
321                if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
322                    getParent().requestDisallowInterceptTouchEvent(true);
323                }
324                break;
325            case MotionEvent.ACTION_POINTER_UP:
326                final int upPointer = event.getPointerId(event.getActionIndex());
327                if (mTrackingPointer == upPointer) {
328                    // gesture is ongoing, find a new pointer to track
329                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
330                    mTrackingPointer = event.getPointerId(newIndex);
331                    mInitialTouchX = event.getX(newIndex);
332                    mInitialTouchY = event.getY(newIndex);
333                }
334                break;
335
336            case MotionEvent.ACTION_MOVE:
337                final float h = y - mInitialTouchY;
338                trackMovement(event);
339                if (mQsTracking) {
340
341                    // Already tracking because onOverscrolled was called. We need to update here
342                    // so we don't stop for a frame until the next touch event gets handled in
343                    // onTouchEvent.
344                    setQsExpansion(h + mInitialHeightOnTouch);
345                    trackMovement(event);
346                    mIntercepting = false;
347                    return true;
348                }
349                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
350                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
351                    onQsExpansionStarted();
352                    mInitialHeightOnTouch = mQsExpansionHeight;
353                    mInitialTouchY = y;
354                    mInitialTouchX = x;
355                    mQsTracking = true;
356                    mIntercepting = false;
357                    return true;
358                }
359                break;
360
361            case MotionEvent.ACTION_CANCEL:
362            case MotionEvent.ACTION_UP:
363                trackMovement(event);
364                if (mQsTracking) {
365                    flingQsWithCurrentVelocity();
366                    mQsTracking = false;
367                }
368                mIntercepting = false;
369                break;
370        }
371        return !mQsExpanded && super.onInterceptTouchEvent(event);
372    }
373
374    @Override
375    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
376
377        // Block request when interacting with the scroll view so we can still intercept the
378        // scrolling when QS is expanded.
379        if (mScrollView.isDispatchingTouchEvent()) {
380            return;
381        }
382        super.requestDisallowInterceptTouchEvent(disallowIntercept);
383    }
384
385    private void flingQsWithCurrentVelocity() {
386        float vel = getCurrentVelocity();
387
388        // TODO: Better logic whether we should expand or not.
389        flingSettings(vel, vel > 0);
390    }
391
392    @Override
393    public boolean onTouchEvent(MotionEvent event) {
394        if (mBlockTouches) {
395            return false;
396        }
397        // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
398        // implementation.
399        if (!mIsExpanding && !mQsExpanded && mStatusBar.getBarState() != StatusBarState.SHADE) {
400            mPageSwiper.onTouchEvent(event);
401            if (mPageSwiper.isSwipingInProgress()) {
402                return true;
403            }
404        }
405        if (mQsTracking || mQsExpanded) {
406            return onQsTouch(event);
407        }
408
409        super.onTouchEvent(event);
410        return true;
411    }
412
413    @Override
414    protected boolean hasConflictingGestures() {
415        return mStatusBar.getBarState() != StatusBarState.SHADE;
416    }
417
418    private boolean onQsTouch(MotionEvent event) {
419        int pointerIndex = event.findPointerIndex(mTrackingPointer);
420        if (pointerIndex < 0) {
421            pointerIndex = 0;
422            mTrackingPointer = event.getPointerId(pointerIndex);
423        }
424        final float y = event.getY(pointerIndex);
425        final float x = event.getX(pointerIndex);
426
427        switch (event.getActionMasked()) {
428            case MotionEvent.ACTION_DOWN:
429                mQsTracking = true;
430                mInitialTouchY = y;
431                mInitialTouchX = x;
432                onQsExpansionStarted();
433                mInitialHeightOnTouch = mQsExpansionHeight;
434                initVelocityTracker();
435                trackMovement(event);
436                break;
437
438            case MotionEvent.ACTION_POINTER_UP:
439                final int upPointer = event.getPointerId(event.getActionIndex());
440                if (mTrackingPointer == upPointer) {
441                    // gesture is ongoing, find a new pointer to track
442                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
443                    final float newY = event.getY(newIndex);
444                    final float newX = event.getX(newIndex);
445                    mTrackingPointer = event.getPointerId(newIndex);
446                    mInitialHeightOnTouch = mQsExpansionHeight;
447                    mInitialTouchY = newY;
448                    mInitialTouchX = newX;
449                }
450                break;
451
452            case MotionEvent.ACTION_MOVE:
453                final float h = y - mInitialTouchY;
454                setQsExpansion(h + mInitialHeightOnTouch);
455                trackMovement(event);
456                break;
457
458            case MotionEvent.ACTION_UP:
459            case MotionEvent.ACTION_CANCEL:
460                mQsTracking = false;
461                mTrackingPointer = -1;
462                trackMovement(event);
463                flingQsWithCurrentVelocity();
464                if (mVelocityTracker != null) {
465                    mVelocityTracker.recycle();
466                    mVelocityTracker = null;
467                }
468                break;
469        }
470        return true;
471    }
472
473    @Override
474    public void onOverscrolled(int amount) {
475        if (mIntercepting) {
476            onQsExpansionStarted(amount);
477            mInitialHeightOnTouch = mQsExpansionHeight;
478            mInitialTouchY = mLastTouchY;
479            mInitialTouchX = mLastTouchX;
480            mQsTracking = true;
481        }
482    }
483
484    private void onQsExpansionStarted() {
485        onQsExpansionStarted(0);
486    }
487
488    private void onQsExpansionStarted(int overscrollAmount) {
489        cancelAnimation();
490
491        // Reset scroll position and apply that position to the expanded height.
492        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
493        mScrollView.scrollTo(0, 0);
494        setQsExpansion(height);
495    }
496
497    private void setQsExpanded(boolean expanded) {
498        boolean changed = mQsExpanded != expanded;
499        if (changed) {
500            mQsExpanded = expanded;
501            updateQsState();
502        }
503    }
504
505    public void setKeyguardShowing(boolean keyguardShowing) {
506        mKeyguardShowing = keyguardShowing;
507        updateQsState();
508    }
509
510    private void updateQsState() {
511        mHeader.setExpanded(mQsExpanded);
512        mNotificationStackScroller.setEnabled(!mQsExpanded);
513        mQsPanel.setVisibility(mQsExpanded ? View.VISIBLE : View.INVISIBLE);
514        mQsContainer.setVisibility(mKeyguardShowing && !mQsExpanded
515                ? View.INVISIBLE
516                : View.VISIBLE);
517        mScrollView.setTouchEnabled(mQsExpanded);
518    }
519
520    private void setQsExpansion(float height) {
521        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
522        if (height > mQsMinExpansionHeight && !mQsExpanded) {
523            setQsExpanded(true);
524        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
525            setQsExpanded(false);
526        }
527        mQsExpansionHeight = height;
528        mHeader.setExpansion(height - mQsPeekHeight);
529        setQsTranslation(height);
530        setQsStackScrollerPadding(height);
531        mStatusBar.userActivity();
532    }
533
534    private void setQsTranslation(float height) {
535        mQsContainer.setY(height - mQsContainer.getHeight());
536    }
537
538    private void setQsStackScrollerPadding(float height) {
539        float start = height - mScrollView.getScrollY() + mNotificationTopPadding;
540        float stackHeight = mNotificationStackScroller.getHeight() - start;
541        if (stackHeight <= mMinStackHeight) {
542            float overflow = mMinStackHeight - stackHeight;
543            stackHeight = mMinStackHeight;
544            start = mNotificationStackScroller.getHeight() - stackHeight;
545            mNotificationStackScroller.setTranslationY(overflow);
546            mNotificationTranslation = overflow + mScrollView.getScrollY();
547        } else {
548            mNotificationStackScroller.setTranslationY(0);
549            mNotificationTranslation = mScrollView.getScrollY();
550        }
551        mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false);
552    }
553
554    private int clampQsStackScrollerPadding(int desiredPadding) {
555        return Math.max(desiredPadding, mStackScrollerIntrinsicPadding);
556    }
557
558    private void trackMovement(MotionEvent event) {
559        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
560        mLastTouchX = event.getX();
561        mLastTouchY = event.getY();
562    }
563
564    private void initVelocityTracker() {
565        if (mVelocityTracker != null) {
566            mVelocityTracker.recycle();
567        }
568        mVelocityTracker = VelocityTracker.obtain();
569    }
570
571    private float getCurrentVelocity() {
572        if (mVelocityTracker == null) {
573            return 0;
574        }
575        mVelocityTracker.computeCurrentVelocity(1000);
576        return mVelocityTracker.getYVelocity();
577    }
578
579    private void cancelAnimation() {
580        if (mQsExpansionAnimator != null) {
581            mQsExpansionAnimator.cancel();
582        }
583    }
584    private void flingSettings(float vel, boolean expand) {
585        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
586        if (target == mQsExpansionHeight) {
587            return;
588        }
589        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
590        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
591        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
592            @Override
593            public void onAnimationUpdate(ValueAnimator animation) {
594                setQsExpansion((Float) animation.getAnimatedValue());
595            }
596        });
597        animator.addListener(new AnimatorListenerAdapter() {
598            @Override
599            public void onAnimationEnd(Animator animation) {
600                mQsExpansionAnimator = null;
601            }
602        });
603        animator.start();
604        mQsExpansionAnimator = animator;
605    }
606
607    /**
608     * @return Whether we should intercept a gesture to open Quick Settings.
609     */
610    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
611        if (!mQsExpansionEnabled) {
612            return false;
613        }
614        boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight()
615                && y >= mHeader.getTop() && y <= mHeader.getBottom();
616        if (mQsExpanded) {
617            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
618        } else {
619            return onHeader;
620        }
621    }
622
623    @Override
624    public void setVisibility(int visibility) {
625        int oldVisibility = getVisibility();
626        super.setVisibility(visibility);
627        if (visibility != oldVisibility) {
628            reparentStatusIcons(visibility == VISIBLE);
629        }
630    }
631
632    /**
633     * When the notification panel gets expanded, we need to move the status icons in the header
634     * card.
635     */
636    private void reparentStatusIcons(boolean toHeader) {
637        if (mStatusBar == null) {
638            return;
639        }
640        LinearLayout systemIcons = mStatusBar.getSystemIcons();
641        if (systemIcons.getParent() != null) {
642            ((ViewGroup) systemIcons.getParent()).removeView(systemIcons);
643        }
644        if (toHeader) {
645            mHeader.attachSystemIcons(systemIcons);
646        } else {
647            mHeader.onSystemIconsDetached();
648            mStatusBar.reattachSystemIcons();
649        }
650    }
651
652    @Override
653    protected boolean isScrolledToBottom() {
654        if (!isInSettings()) {
655            return mNotificationStackScroller.isScrolledToBottom();
656        }
657        return super.isScrolledToBottom();
658    }
659
660    @Override
661    protected int getMaxPanelHeight() {
662        // TODO: Figure out transition for collapsing when QS is open, adjust height here.
663        int maxPanelHeight = super.getMaxPanelHeight();
664        int emptyBottomMargin = mStackScrollerContainer.getHeight()
665                - mNotificationStackScroller.getHeight()
666                + mNotificationStackScroller.getEmptyBottomMargin();
667        int maxHeight = maxPanelHeight - emptyBottomMargin - mTopPaddingAdjustment;
668        maxHeight = Math.max(maxHeight, mStatusBarMinHeight);
669        return maxHeight;
670    }
671
672    private boolean isInSettings() {
673        return mQsExpanded;
674    }
675
676    @Override
677    protected void onHeightUpdated(float expandedHeight) {
678        if (!mQsExpanded) {
679            positionClockAndNotifications();
680        }
681        mNotificationStackScroller.setStackHeight(expandedHeight);
682    }
683
684    @Override
685    protected void onExpandingStarted() {
686        super.onExpandingStarted();
687        mNotificationStackScroller.onExpansionStarted();
688        mIsExpanding = true;
689    }
690
691    @Override
692    protected void onExpandingFinished() {
693        super.onExpandingFinished();
694        mNotificationStackScroller.onExpansionStopped();
695        mIsExpanding = false;
696    }
697
698    @Override
699    protected void onOverExpansionChanged(float overExpansion) {
700        float currentOverScroll = mNotificationStackScroller.getCurrentOverScrolledPixels(true);
701        mNotificationStackScroller.setOverScrolledPixels(currentOverScroll + overExpansion
702                        - mOverExpansion, true /* onTop */, false /* animate */);
703        super.onOverExpansionChanged(overExpansion);
704    }
705
706    @Override
707    protected void onTrackingStopped(boolean expand) {
708        super.onTrackingStopped(expand);
709        mOverExpansion = 0.0f;
710        mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */,
711                true /* animate */);
712    }
713
714
715    @Override
716    public void onHeightChanged(ExpandableView view) {
717        requestPanelHeightUpdate();
718    }
719
720    @Override
721    public void onScrollChanged() {
722        if (mQsExpanded) {
723            mNotificationStackScroller.setTranslationY(
724                    mNotificationTranslation - mScrollView.getScrollY());
725        }
726    }
727
728    @Override
729    protected void onConfigurationChanged(Configuration newConfig) {
730        super.onConfigurationChanged(newConfig);
731        mPageSwiper.onConfigurationChanged();
732    }
733
734    @Override
735    public void onClick(View v) {
736        if (v == mHeader.getBackgroundView()) {
737            onQsExpansionStarted();
738            if (mQsExpanded) {
739                flingSettings(0 /* vel */, false /* expand */);
740            } else if (mQsExpansionEnabled) {
741                flingSettings(0 /* vel */, true /* expand */);
742            }
743        }
744    }
745
746    @Override
747    public void onAnimationToSideStarted(boolean rightPage) {
748        if (rightPage) {
749            mKeyguardBottomArea.launchCamera();
750        } else {
751            mKeyguardBottomArea.launchPhone();
752        }
753        mBlockTouches = true;
754    }
755
756
757    @Override
758    public float getPageWidth() {
759        return getWidth();
760    }
761
762    @Override
763    public ArrayList<View> getTranslationViews() {
764        return mSwipeTranslationViews;
765    }
766
767    @Override
768    public View getLeftIcon() {
769        return mKeyguardBottomArea.getPhoneImageView();
770    }
771
772    @Override
773    public View getCenterIcon() {
774        return mKeyguardBottomArea.getLockIcon();
775    }
776
777    @Override
778    public View getRightIcon() {
779        return mKeyguardBottomArea.getCameraImageView();
780    }
781}
782