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