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