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