NotificationPanelView.java revision b572e4c77711a6a92b244f1b9edbf13710481184
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;
35import android.widget.TextView;
36
37import com.android.systemui.R;
38import com.android.systemui.qs.QSPanel;
39import com.android.systemui.statusbar.ExpandableView;
40import com.android.systemui.statusbar.FlingAnimationUtils;
41import com.android.systemui.statusbar.GestureRecorder;
42import com.android.systemui.statusbar.MirrorView;
43import com.android.systemui.statusbar.StatusBarState;
44import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
45import com.android.systemui.statusbar.stack.StackStateAnimator;
46
47import java.util.ArrayList;
48
49public class NotificationPanelView extends PanelView implements
50        ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
51        View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
52        KeyguardPageSwipeHelper.Callback {
53
54    // Cap and total height of Roboto font. Needs to be adjusted when font for the big clock is
55    // changed.
56    private static final int CAP_HEIGHT = 1456;
57    private static final int FONT_HEIGHT = 2163;
58
59    private static final float HEADER_RUBBERBAND_FACTOR = 2.15f;
60    private static final float LOCK_ICON_ACTIVE_SCALE = 1.2f;
61
62    private KeyguardPageSwipeHelper mPageSwiper;
63    private StatusBarHeaderView mHeader;
64    private View mQsContainer;
65    private QSPanel mQsPanel;
66    private View mKeyguardStatusView;
67    private ObservableScrollView mScrollView;
68    private TextView mClockView;
69    private View mReserveNotificationSpace;
70    private MirrorView mSystemIconsCopy;
71
72    private NotificationStackScrollLayout mNotificationStackScroller;
73    private int mNotificationTopPadding;
74    private boolean mAnimateNextTopPaddingChange;
75
76    private int mTrackingPointer;
77    private VelocityTracker mVelocityTracker;
78    private boolean mQsTracking;
79
80    /**
81     * If set, the ongoing touch gesture might both trigger the expansion in {@link PanelView} and
82     * the expansion for quick settings.
83     */
84    private boolean mConflictingQsExpansionGesture;
85
86    /**
87     * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't
88     * intercepted yet.
89     */
90    private boolean mIntercepting;
91    private boolean mQsExpanded;
92    private boolean mQsExpandedWhenExpandingStarted;
93    private boolean mQsFullyExpanded;
94    private boolean mKeyguardShowing;
95    private int mStatusBarState;
96    private float mInitialHeightOnTouch;
97    private float mInitialTouchX;
98    private float mInitialTouchY;
99    private float mLastTouchX;
100    private float mLastTouchY;
101    private float mQsExpansionHeight;
102    private int mQsMinExpansionHeight;
103    private int mQsMaxExpansionHeight;
104    private int mQsPeekHeight;
105    private boolean mStackScrollerOverscrolling;
106    private boolean mQsExpansionFromOverscroll;
107    private boolean mQsExpansionEnabled = true;
108    private ValueAnimator mQsExpansionAnimator;
109    private FlingAnimationUtils mFlingAnimationUtils;
110    private int mStatusBarMinHeight;
111    private boolean mUnlockIconActive;
112    private int mNotificationsHeaderCollideDistance;
113    private int mUnlockMoveDistance;
114
115    private Interpolator mFastOutSlowInInterpolator;
116    private Interpolator mFastOutLinearInterpolator;
117    private ObjectAnimator mClockAnimator;
118    private int mClockAnimationTarget = -1;
119    private int mTopPaddingAdjustment;
120    private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
121            new KeyguardClockPositionAlgorithm();
122    private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
123            new KeyguardClockPositionAlgorithm.Result();
124    private boolean mIsExpanding;
125
126    private boolean mBlockTouches;
127    private ArrayList<View> mSwipeTranslationViews = new ArrayList<>();
128    private int mNotificationScrimWaitDistance;
129
130    /**
131     * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still
132     * need to take this into account in our panel height calculation.
133     */
134    private int mScrollYOverride = -1;
135    private boolean mQsAnimatorExpand;
136
137    public NotificationPanelView(Context context, AttributeSet attrs) {
138        super(context, attrs);
139        mSystemIconsCopy = new MirrorView(context);
140    }
141
142    public void setStatusBar(PhoneStatusBar bar) {
143        mStatusBar = bar;
144    }
145
146    @Override
147    protected void onFinishInflate() {
148        super.onFinishInflate();
149        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
150        mHeader.getBackgroundView().setOnClickListener(this);
151        mHeader.setOverlayParent(this);
152        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
153        mQsContainer = findViewById(R.id.quick_settings_container);
154        mQsPanel = (QSPanel) findViewById(R.id.quick_settings_panel);
155        mClockView = (TextView) findViewById(R.id.clock_view);
156        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
157        mScrollView.setListener(this);
158        mReserveNotificationSpace = findViewById(R.id.reserve_notification_space);
159        mNotificationStackScroller = (NotificationStackScrollLayout)
160                findViewById(R.id.notification_stack_scroller);
161        mNotificationStackScroller.setOnHeightChangedListener(this);
162        mNotificationStackScroller.setOverscrollTopChangedListener(this);
163        mNotificationStackScroller.setScrollView(mScrollView);
164        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
165                android.R.interpolator.fast_out_slow_in);
166        mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(),
167                android.R.interpolator.fast_out_linear_in);
168        mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area);
169        mSwipeTranslationViews.add(mNotificationStackScroller);
170        mSwipeTranslationViews.add(mKeyguardStatusView);
171        mPageSwiper = new KeyguardPageSwipeHelper(this, getContext());
172    }
173
174    @Override
175    protected void loadDimens() {
176        super.loadDimens();
177        mNotificationTopPadding = getResources().getDimensionPixelSize(
178                R.dimen.notifications_top_padding);
179        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
180        mStatusBarMinHeight = getResources().getDimensionPixelSize(
181                com.android.internal.R.dimen.status_bar_height);
182        mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
183        mNotificationsHeaderCollideDistance =
184                getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance);
185        mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance);
186        mClockPositionAlgorithm.loadDimens(getResources());
187        mNotificationScrimWaitDistance =
188                getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance);
189    }
190
191    @Override
192    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
193        super.onLayout(changed, left, top, right, bottom);
194
195        // Update Clock Pivot
196        mKeyguardStatusView.setPivotX(getWidth() / 2);
197        mKeyguardStatusView.setPivotY(
198                (FONT_HEIGHT - CAP_HEIGHT) / 2048f * mClockView.getTextSize());
199
200        // Calculate quick setting heights.
201        mQsMinExpansionHeight = mHeader.getCollapsedHeight() + mQsPeekHeight;
202        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
203        if (mQsExpanded) {
204            if (mQsFullyExpanded) {
205                mQsExpansionHeight = mQsMaxExpansionHeight;
206                requestScrollerTopPaddingUpdate(false /* animate */);
207            }
208        } else {
209            if (!mStackScrollerOverscrolling) {
210                setQsExpansion(mQsMinExpansionHeight);
211            }
212            positionClockAndNotifications();
213            mNotificationStackScroller.setStackHeight(getExpandedHeight());
214        }
215    }
216
217    /**
218     * Positions the clock and notifications dynamically depending on how many notifications are
219     * showing.
220     */
221    private void positionClockAndNotifications() {
222        boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
223        int stackScrollerPadding;
224        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
225            int bottom = mHeader.getCollapsedHeight();
226            stackScrollerPadding = bottom + mQsPeekHeight
227                    + mNotificationTopPadding;
228            mTopPaddingAdjustment = 0;
229        } else {
230            mClockPositionAlgorithm.setup(
231                    mStatusBar.getMaxKeyguardNotifications(),
232                    getMaxPanelHeight(),
233                    getExpandedHeight(),
234                    mNotificationStackScroller.getNotGoneChildCount(),
235                    getHeight(),
236                    mKeyguardStatusView.getHeight());
237            mClockPositionAlgorithm.run(mClockPositionResult);
238            if (animate || mClockAnimator != null) {
239                startClockAnimation(mClockPositionResult.clockY);
240            } else {
241                mKeyguardStatusView.setY(mClockPositionResult.clockY);
242            }
243            updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale);
244            stackScrollerPadding = mClockPositionResult.stackScrollerPadding;
245            mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
246        }
247        mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding);
248        requestScrollerTopPaddingUpdate(animate);
249    }
250
251    private void startClockAnimation(int y) {
252        if (mClockAnimationTarget == y) {
253            return;
254        }
255        mClockAnimationTarget = y;
256        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
257            @Override
258            public boolean onPreDraw() {
259                getViewTreeObserver().removeOnPreDrawListener(this);
260                if (mClockAnimator != null) {
261                    mClockAnimator.removeAllListeners();
262                    mClockAnimator.cancel();
263                }
264                mClockAnimator = ObjectAnimator
265                        .ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget);
266                mClockAnimator.setInterpolator(mFastOutSlowInInterpolator);
267                mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
268                mClockAnimator.addListener(new AnimatorListenerAdapter() {
269                    @Override
270                    public void onAnimationEnd(Animator animation) {
271                        mClockAnimator = null;
272                        mClockAnimationTarget = -1;
273                    }
274                });
275                mClockAnimator.start();
276                return true;
277            }
278        });
279    }
280
281    private void updateClock(float alpha, float scale) {
282        mKeyguardStatusView.setAlpha(alpha);
283        mKeyguardStatusView.setScaleX(scale);
284        mKeyguardStatusView.setScaleY(scale);
285    }
286
287    public void animateToFullShade() {
288        mAnimateNextTopPaddingChange = true;
289        mNotificationStackScroller.goToFullShade();
290        requestLayout();
291    }
292
293    public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
294        mQsExpansionEnabled = qsExpansionEnabled;
295    }
296
297    @Override
298    public void resetViews() {
299        mBlockTouches = false;
300        mUnlockIconActive = false;
301        mPageSwiper.reset();
302        closeQs();
303        mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */,
304                true /* cancelAnimators */);
305    }
306
307    public void closeQs() {
308        cancelAnimation();
309        setQsExpansion(mQsMinExpansionHeight);
310    }
311
312    public void animateCloseQs() {
313        if (mQsExpansionAnimator != null) {
314            if (!mQsAnimatorExpand) {
315                return;
316            }
317            float height = mQsExpansionHeight;
318            mQsExpansionAnimator.cancel();
319            setQsExpansion(height);
320        }
321        flingSettings(0 /* vel */, false);
322    }
323
324    public void openQs() {
325        cancelAnimation();
326        if (mQsExpansionEnabled) {
327            setQsExpansion(mQsMaxExpansionHeight);
328        }
329    }
330
331    @Override
332    public void fling(float vel, boolean always) {
333        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
334        if (gr != null) {
335            gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
336        }
337        super.fling(vel, always);
338    }
339
340    @Override
341    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
342        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
343            event.getText()
344                    .add(getContext().getString(R.string.accessibility_desc_notification_shade));
345            return true;
346        }
347
348        return super.dispatchPopulateAccessibilityEvent(event);
349    }
350
351    @Override
352    public boolean onInterceptTouchEvent(MotionEvent event) {
353        if (mBlockTouches) {
354            return false;
355        }
356        int pointerIndex = event.findPointerIndex(mTrackingPointer);
357        if (pointerIndex < 0) {
358            pointerIndex = 0;
359            mTrackingPointer = event.getPointerId(pointerIndex);
360        }
361        final float x = event.getX(pointerIndex);
362        final float y = event.getY(pointerIndex);
363
364        switch (event.getActionMasked()) {
365            case MotionEvent.ACTION_DOWN:
366                mIntercepting = true;
367                mInitialTouchY = y;
368                mInitialTouchX = x;
369                initVelocityTracker();
370                trackMovement(event);
371                if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
372                    getParent().requestDisallowInterceptTouchEvent(true);
373                }
374                if (mQsExpansionAnimator != null) {
375                    onQsExpansionStarted();
376                    mInitialHeightOnTouch = mQsExpansionHeight;
377                    mQsTracking = true;
378                    mIntercepting = false;
379                    mNotificationStackScroller.removeLongPressCallback();
380                }
381                break;
382            case MotionEvent.ACTION_POINTER_UP:
383                final int upPointer = event.getPointerId(event.getActionIndex());
384                if (mTrackingPointer == upPointer) {
385                    // gesture is ongoing, find a new pointer to track
386                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
387                    mTrackingPointer = event.getPointerId(newIndex);
388                    mInitialTouchX = event.getX(newIndex);
389                    mInitialTouchY = event.getY(newIndex);
390                }
391                break;
392
393            case MotionEvent.ACTION_MOVE:
394                final float h = y - mInitialTouchY;
395                trackMovement(event);
396                if (mQsTracking) {
397
398                    // Already tracking because onOverscrolled was called. We need to update here
399                    // so we don't stop for a frame until the next touch event gets handled in
400                    // onTouchEvent.
401                    setQsExpansion(h + mInitialHeightOnTouch);
402                    trackMovement(event);
403                    mIntercepting = false;
404                    return true;
405                }
406                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
407                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
408                    onQsExpansionStarted();
409                    mInitialHeightOnTouch = mQsExpansionHeight;
410                    mInitialTouchY = y;
411                    mInitialTouchX = x;
412                    mQsTracking = true;
413                    mIntercepting = false;
414                    mNotificationStackScroller.removeLongPressCallback();
415                    return true;
416                }
417                break;
418
419            case MotionEvent.ACTION_CANCEL:
420            case MotionEvent.ACTION_UP:
421                trackMovement(event);
422                if (mQsTracking) {
423                    flingQsWithCurrentVelocity();
424                    mQsTracking = false;
425                }
426                mIntercepting = false;
427                break;
428        }
429        return !mQsExpanded && super.onInterceptTouchEvent(event);
430    }
431
432    @Override
433    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
434
435        // Block request when interacting with the scroll view so we can still intercept the
436        // scrolling when QS is expanded.
437        if (mScrollView.isDispatchingTouchEvent()) {
438            return;
439        }
440        super.requestDisallowInterceptTouchEvent(disallowIntercept);
441    }
442
443    private void flingQsWithCurrentVelocity() {
444        float vel = getCurrentVelocity();
445        flingSettings(vel, flingExpandsQs(vel));
446    }
447
448    private boolean flingExpandsQs(float vel) {
449        if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
450            return getQsExpansionFraction() > 0.5f;
451        } else {
452            return vel > 0;
453        }
454    }
455
456    private float getQsExpansionFraction() {
457        return (mQsExpansionHeight - mQsMinExpansionHeight)
458                / (getTempQsMaxExpansion() - mQsMinExpansionHeight);
459    }
460
461    @Override
462    public boolean onTouchEvent(MotionEvent event) {
463        if (mBlockTouches) {
464            return false;
465        }
466        // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
467        // implementation.
468        if ((!mIsExpanding || mHintAnimationRunning)
469                && !mQsExpanded
470                && mStatusBar.getBarState() != StatusBarState.SHADE) {
471            mPageSwiper.onTouchEvent(event);
472            if (mPageSwiper.isSwipingInProgress()) {
473                return true;
474            }
475        }
476        if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
477                && mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
478
479            // Down in the empty area while fully expanded - go to QS.
480            mQsTracking = true;
481            mConflictingQsExpansionGesture = true;
482            onQsExpansionStarted();
483            mInitialHeightOnTouch = mQsExpansionHeight;
484            mInitialTouchY = event.getX();
485            mInitialTouchX = event.getY();
486        }
487        if (mQsTracking || mQsExpanded) {
488            onQsTouch(event);
489            if (!mConflictingQsExpansionGesture) {
490                return true;
491            }
492        }
493        if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
494                || event.getActionMasked() == MotionEvent.ACTION_UP) {
495            mConflictingQsExpansionGesture = false;
496        }
497        super.onTouchEvent(event);
498        return true;
499    }
500
501    @Override
502    protected boolean flingExpands(float vel, float vectorVel) {
503        boolean expands = super.flingExpands(vel, vectorVel);
504
505        // If we are already running a QS expansion, make sure that we keep the panel open.
506        if (mQsExpansionAnimator != null) {
507            expands = true;
508        }
509        return expands;
510    }
511
512    @Override
513    protected boolean hasConflictingGestures() {
514        return mStatusBar.getBarState() != StatusBarState.SHADE;
515    }
516
517    private void onQsTouch(MotionEvent event) {
518        int pointerIndex = event.findPointerIndex(mTrackingPointer);
519        if (pointerIndex < 0) {
520            pointerIndex = 0;
521            mTrackingPointer = event.getPointerId(pointerIndex);
522        }
523        final float y = event.getY(pointerIndex);
524        final float x = event.getX(pointerIndex);
525
526        switch (event.getActionMasked()) {
527            case MotionEvent.ACTION_DOWN:
528                mQsTracking = true;
529                mInitialTouchY = y;
530                mInitialTouchX = x;
531                onQsExpansionStarted();
532                mInitialHeightOnTouch = mQsExpansionHeight;
533                initVelocityTracker();
534                trackMovement(event);
535                break;
536
537            case MotionEvent.ACTION_POINTER_UP:
538                final int upPointer = event.getPointerId(event.getActionIndex());
539                if (mTrackingPointer == upPointer) {
540                    // gesture is ongoing, find a new pointer to track
541                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
542                    final float newY = event.getY(newIndex);
543                    final float newX = event.getX(newIndex);
544                    mTrackingPointer = event.getPointerId(newIndex);
545                    mInitialHeightOnTouch = mQsExpansionHeight;
546                    mInitialTouchY = newY;
547                    mInitialTouchX = newX;
548                }
549                break;
550
551            case MotionEvent.ACTION_MOVE:
552                final float h = y - mInitialTouchY;
553                setQsExpansion(h + mInitialHeightOnTouch);
554                trackMovement(event);
555                break;
556
557            case MotionEvent.ACTION_UP:
558            case MotionEvent.ACTION_CANCEL:
559                mQsTracking = false;
560                mTrackingPointer = -1;
561                trackMovement(event);
562                float fraction = getQsExpansionFraction();
563                if ((fraction != 0f || y >= mInitialTouchY)
564                        && (fraction != 1f || y <= mInitialTouchY)) {
565                    flingQsWithCurrentVelocity();
566                } else {
567                    mScrollYOverride = -1;
568                }
569                if (mVelocityTracker != null) {
570                    mVelocityTracker.recycle();
571                    mVelocityTracker = null;
572                }
573                break;
574        }
575    }
576
577    @Override
578    public void onOverscrolled(int amount) {
579        if (mIntercepting) {
580            onQsExpansionStarted(amount);
581            mInitialHeightOnTouch = mQsExpansionHeight;
582            mInitialTouchY = mLastTouchY;
583            mInitialTouchX = mLastTouchX;
584            mQsTracking = true;
585        }
586    }
587
588    @Override
589    public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
590        cancelAnimation();
591        float rounded = amount >= 1f ? amount : 0f;
592        mStackScrollerOverscrolling = rounded != 0f && isRubberbanded;
593        mQsExpansionFromOverscroll = rounded != 0f;
594        setQsExpansion(mQsMinExpansionHeight + rounded);
595        updateQsState();
596    }
597
598    @Override
599    public void flingTopOverscroll(float velocity, boolean open) {
600        setQsExpansion(mQsExpansionHeight);
601        flingSettings(velocity, open, new Runnable() {
602            @Override
603            public void run() {
604                mStackScrollerOverscrolling = false;
605                mQsExpansionFromOverscroll = false;
606                updateQsState();
607            }
608        });
609    }
610
611    private void onQsExpansionStarted() {
612        onQsExpansionStarted(0);
613    }
614
615    private void onQsExpansionStarted(int overscrollAmount) {
616        cancelAnimation();
617
618        // Reset scroll position and apply that position to the expanded height.
619        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
620        if (mScrollView.getScrollY() != 0) {
621            mScrollYOverride = mScrollView.getScrollY();
622        }
623        mScrollView.scrollTo(0, 0);
624        setQsExpansion(height);
625    }
626
627    private void setQsExpanded(boolean expanded) {
628        boolean changed = mQsExpanded != expanded;
629        if (changed) {
630            mQsExpanded = expanded;
631            updateQsState();
632            requestPanelHeightUpdate();
633            mNotificationStackScroller.setInterceptDelegateEnabled(expanded);
634            mStatusBar.setQsExpanded(expanded);
635        }
636    }
637
638    public void setBarState(int statusBarState) {
639        boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD
640                || statusBarState == StatusBarState.SHADE_LOCKED;
641        if (!mKeyguardShowing && keyguardShowing) {
642            setQsTranslation(mQsExpansionHeight);
643            mHeader.setTranslationY(0f);
644        }
645        mStatusBarState = statusBarState;
646        mKeyguardShowing = keyguardShowing;
647        updateQsState();
648    }
649
650    private void updateQsState() {
651        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling;
652        mHeader.setExpanded(expandVisually, mStackScrollerOverscrolling);
653        mNotificationStackScroller.setScrollingEnabled(mStatusBarState != StatusBarState.KEYGUARD
654                && (!mQsExpanded || mQsExpansionFromOverscroll));
655        mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
656        mQsContainer.setVisibility(
657                mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE);
658        mScrollView.setTouchEnabled(mQsExpanded);
659    }
660
661    private void setQsExpansion(float height) {
662        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
663        mQsFullyExpanded = height == mQsMaxExpansionHeight;
664        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
665            setQsExpanded(true);
666        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
667            setQsExpanded(false);
668        }
669        mQsExpansionHeight = height;
670        mHeader.setExpansion(getQsExpansionFraction());
671        setQsTranslation(height);
672        requestScrollerTopPaddingUpdate(false /* animate */);
673        updateNotificationScrim(height);
674    }
675
676    private void updateNotificationScrim(float height) {
677        int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance;
678        float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance);
679        progress = Math.max(0.0f, Math.min(progress, 1.0f));
680        mNotificationStackScroller.setScrimAlpha(progress);
681    }
682
683    private void setQsTranslation(float height) {
684        mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation());
685    }
686
687    private void requestScrollerTopPaddingUpdate(boolean animate) {
688        mNotificationStackScroller.updateTopPadding(mQsExpansionHeight,
689                mScrollView.getScrollY(),
690                mAnimateNextTopPaddingChange || animate);
691        mAnimateNextTopPaddingChange = false;
692    }
693
694    private void trackMovement(MotionEvent event) {
695        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
696        mLastTouchX = event.getX();
697        mLastTouchY = event.getY();
698    }
699
700    private void initVelocityTracker() {
701        if (mVelocityTracker != null) {
702            mVelocityTracker.recycle();
703        }
704        mVelocityTracker = VelocityTracker.obtain();
705    }
706
707    private float getCurrentVelocity() {
708        if (mVelocityTracker == null) {
709            return 0;
710        }
711        mVelocityTracker.computeCurrentVelocity(1000);
712        return mVelocityTracker.getYVelocity();
713    }
714
715    private void cancelAnimation() {
716        if (mQsExpansionAnimator != null) {
717            mQsExpansionAnimator.cancel();
718        }
719    }
720
721    private void flingSettings(float vel, boolean expand) {
722        flingSettings(vel, expand, null);
723    }
724
725    private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) {
726        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
727        if (target == mQsExpansionHeight) {
728            mScrollYOverride = -1;
729            if (onFinishRunnable != null) {
730                onFinishRunnable.run();
731            }
732            return;
733        }
734        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
735        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
736        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
737            @Override
738            public void onAnimationUpdate(ValueAnimator animation) {
739                setQsExpansion((Float) animation.getAnimatedValue());
740            }
741        });
742        animator.addListener(new AnimatorListenerAdapter() {
743            @Override
744            public void onAnimationEnd(Animator animation) {
745                mScrollYOverride = -1;
746                mQsExpansionAnimator = null;
747                if (onFinishRunnable != null) {
748                    onFinishRunnable.run();
749                }
750            }
751        });
752        animator.start();
753        mQsExpansionAnimator = animator;
754        mQsAnimatorExpand = expand;
755    }
756
757    /**
758     * @return Whether we should intercept a gesture to open Quick Settings.
759     */
760    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
761        if (!mQsExpansionEnabled) {
762            return false;
763        }
764        boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight()
765                && y >= mHeader.getTop() && y <= mHeader.getBottom();
766        if (mQsExpanded) {
767            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
768        } else {
769            return onHeader;
770        }
771    }
772
773    @Override
774    public void setVisibility(int visibility) {
775        int oldVisibility = getVisibility();
776        super.setVisibility(visibility);
777        if (visibility != oldVisibility) {
778            reparentStatusIcons(visibility == VISIBLE);
779        }
780    }
781
782    /**
783     * When the notification panel gets expanded, we need to move the status icons in the header
784     * card.
785     */
786    private void reparentStatusIcons(boolean toHeader) {
787        if (mStatusBar == null) {
788            return;
789        }
790        LinearLayout systemIcons = mStatusBar.getSystemIcons();
791        ViewGroup parent = ((ViewGroup) systemIcons.getParent());
792        if (toHeader) {
793            int index = parent.indexOfChild(systemIcons);
794            parent.removeView(systemIcons);
795            mSystemIconsCopy.setMirroredView(
796                    systemIcons, systemIcons.getWidth(), systemIcons.getHeight());
797            parent.addView(mSystemIconsCopy, index);
798            mHeader.attachSystemIcons(systemIcons);
799        } else {
800            ViewGroup newParent = mStatusBar.getSystemIconArea();
801            int index = newParent.indexOfChild(mSystemIconsCopy);
802            parent.removeView(systemIcons);
803            mHeader.onSystemIconsDetached();
804            mSystemIconsCopy.setMirroredView(null, 0, 0);
805            newParent.removeView(mSystemIconsCopy);
806            newParent.addView(systemIcons, index);
807        }
808    }
809
810    @Override
811    protected boolean isScrolledToBottom() {
812        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
813            return true;
814        }
815        if (!isInSettings()) {
816            return mNotificationStackScroller.isScrolledToBottom();
817        }
818        return super.isScrolledToBottom();
819    }
820
821    @Override
822    protected int getMaxPanelHeight() {
823        int min = mStatusBarMinHeight;
824        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD
825                && mNotificationStackScroller.getNotGoneChildCount() == 0) {
826            int minHeight = (int) ((mQsMinExpansionHeight + getOverExpansionAmount())
827                    * HEADER_RUBBERBAND_FACTOR);
828            min = Math.max(min, minHeight);
829        }
830        int maxHeight;
831        if (mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) {
832            maxHeight = (int) calculatePanelHeightQsExpanded();
833        } else {
834            int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin();
835            maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin
836                    - mTopPaddingAdjustment;
837            maxHeight += mNotificationStackScroller.getTopPaddingOverflow();
838        }
839        maxHeight = Math.max(maxHeight, min);
840        return maxHeight;
841    }
842
843    private boolean isInSettings() {
844        return mQsExpanded;
845    }
846
847    @Override
848    protected void onHeightUpdated(float expandedHeight) {
849        if (!mQsExpanded) {
850            positionClockAndNotifications();
851        }
852        if (mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
853                && !mQsExpansionFromOverscroll) {
854            float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding()
855                    + mNotificationStackScroller.getMinStackHeight()
856                    + mNotificationStackScroller.getNotificationTopPadding();
857            float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
858            float t = (expandedHeight - panelHeightQsCollapsed)
859                    / (panelHeightQsExpanded - panelHeightQsCollapsed);
860
861            setQsExpansion(mQsMinExpansionHeight
862                    + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight));
863        }
864        mNotificationStackScroller.setStackHeight(expandedHeight);
865        updateHeader();
866        updateUnlockIcon();
867        updateNotificationTranslucency();
868    }
869
870    /**
871     * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when
872     *         collapsing QS / the panel when QS was scrolled
873     */
874    private int getTempQsMaxExpansion() {
875        int qsTempMaxExpansion = mQsMaxExpansionHeight;
876        if (mScrollYOverride != -1) {
877            qsTempMaxExpansion -= mScrollYOverride;
878        }
879        return qsTempMaxExpansion;
880    }
881
882    private float calculatePanelHeightQsExpanded() {
883        float notificationHeight = mNotificationStackScroller.getHeight()
884                - mNotificationStackScroller.getEmptyBottomMargin()
885                - mNotificationStackScroller.getTopPadding();
886        float totalHeight = mQsMaxExpansionHeight + notificationHeight
887                + mNotificationStackScroller.getNotificationTopPadding();
888        if (totalHeight > mNotificationStackScroller.getHeight()) {
889            float fullyCollapsedHeight = mQsMaxExpansionHeight
890                    + mNotificationStackScroller.getMinStackHeight()
891                    + mNotificationStackScroller.getNotificationTopPadding()
892                    - getScrollViewScrollY();
893            totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight());
894        }
895        return totalHeight;
896    }
897
898    private int getScrollViewScrollY() {
899        if (mScrollYOverride != -1) {
900            return mScrollYOverride;
901        } else {
902            return mScrollView.getScrollY();
903        }
904    }
905    private void updateNotificationTranslucency() {
906        float alpha = (mNotificationStackScroller.getNotificationsTopY()
907                + mNotificationStackScroller.getItemHeight())
908                / (mQsMinExpansionHeight
909                        + mNotificationStackScroller.getItemHeight() / 2);
910        alpha = Math.max(0, Math.min(alpha, 1));
911        alpha = (float) Math.pow(alpha, 0.75);
912
913        // TODO: Draw a rect with DST_OUT over the notifications to achieve the same effect -
914        // this would be much more efficient.
915        mNotificationStackScroller.setAlpha(alpha);
916    }
917
918    @Override
919    protected float getOverExpansionAmount() {
920        return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */);
921    }
922
923    @Override
924    protected float getOverExpansionPixels() {
925        return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */);
926    }
927
928    private void updateUnlockIcon() {
929        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
930                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
931            boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance;
932            if (active && !mUnlockIconActive && mTracking) {
933                mKeyguardBottomArea.getLockIcon().animate()
934                        .alpha(1f)
935                        .scaleY(LOCK_ICON_ACTIVE_SCALE)
936                        .scaleX(LOCK_ICON_ACTIVE_SCALE)
937                        .setInterpolator(mFastOutLinearInterpolator)
938                        .setDuration(150);
939            } else if (!active && mUnlockIconActive && mTracking) {
940                mKeyguardBottomArea.getLockIcon().animate()
941                        .alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT)
942                        .scaleY(1f)
943                        .scaleX(1f)
944                        .setInterpolator(mFastOutLinearInterpolator)
945                        .setDuration(150);
946            }
947            mUnlockIconActive = active;
948        }
949    }
950
951    /**
952     * Hides the header when notifications are colliding with it.
953     */
954    private void updateHeader() {
955        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
956                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
957            updateHeaderKeyguard();
958        } else {
959            updateHeaderShade();
960        }
961
962    }
963
964    private void updateHeaderShade() {
965        mHeader.setAlpha(1f);
966        mHeader.setTranslationY(getHeaderTranslation());
967        setQsTranslation(mQsExpansionHeight);
968    }
969
970    private float getHeaderTranslation() {
971        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
972                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
973            return 0;
974        }
975        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
976            if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) {
977                return 0;
978            } else {
979                return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight;
980            }
981        }
982        return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR;
983    }
984
985    private void updateHeaderKeyguard() {
986        mHeader.setTranslationY(0f);
987        float alpha;
988        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
989
990            // When on Keyguard, we hide the header as soon as the top card of the notification
991            // stack scroller is close enough (collision distance) to the bottom of the header.
992            alpha = mNotificationStackScroller.getNotificationsTopY()
993                    /
994                    (mQsMinExpansionHeight + mNotificationsHeaderCollideDistance);
995
996        } else {
997
998            // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
999            // soon as we start translating the stack.
1000            alpha = mNotificationStackScroller.getNotificationsTopY() / mQsMinExpansionHeight;
1001        }
1002        alpha = Math.max(0, Math.min(alpha, 1));
1003        alpha = (float) Math.pow(alpha, 0.75);
1004        mHeader.setAlpha(alpha);
1005        mKeyguardBottomArea.setAlpha(alpha);
1006        setQsTranslation(mQsExpansionHeight);
1007    }
1008
1009    @Override
1010    protected void onExpandingStarted() {
1011        super.onExpandingStarted();
1012        mNotificationStackScroller.onExpansionStarted();
1013        mIsExpanding = true;
1014        mQsExpandedWhenExpandingStarted = mQsExpanded;
1015        if (mQsExpanded) {
1016            onQsExpansionStarted();
1017        }
1018    }
1019
1020    @Override
1021    protected void onExpandingFinished() {
1022        super.onExpandingFinished();
1023        mNotificationStackScroller.onExpansionStopped();
1024        mIsExpanding = false;
1025        mScrollYOverride = -1;
1026        if (mExpandedHeight == 0f) {
1027            mHeader.setListening(false);
1028            mQsPanel.setListening(false);
1029        } else {
1030            mHeader.setListening(true);
1031            mQsPanel.setListening(true);
1032        }
1033    }
1034
1035    @Override
1036    public void instantExpand() {
1037        super.instantExpand();
1038        mHeader.setListening(true);
1039        mQsPanel.setListening(true);
1040    }
1041
1042    @Override
1043    protected void setOverExpansion(float overExpansion, boolean isPixels) {
1044        if (mConflictingQsExpansionGesture) {
1045            return;
1046        }
1047        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
1048            mNotificationStackScroller.setOnHeightChangedListener(null);
1049            if (isPixels) {
1050                mNotificationStackScroller.setOverScrolledPixels(
1051                        overExpansion, true /* onTop */, false /* animate */);
1052            } else {
1053                mNotificationStackScroller.setOverScrollAmount(
1054                        overExpansion, true /* onTop */, false /* animate */);
1055            }
1056            mNotificationStackScroller.setOnHeightChangedListener(this);
1057        }
1058    }
1059
1060    @Override
1061    protected void onTrackingStarted() {
1062        super.onTrackingStarted();
1063        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1064                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1065            mPageSwiper.animateHideLeftRightIcon();
1066        }
1067    }
1068
1069    @Override
1070    protected void onTrackingStopped(boolean expand) {
1071        super.onTrackingStopped(expand);
1072        if (expand) {
1073            mNotificationStackScroller.setOverScrolledPixels(
1074                    0.0f, true /* onTop */, true /* animate */);
1075        }
1076        if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1077                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1078            mPageSwiper.showAllIcons(true);
1079        }
1080        if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1081                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1082            mKeyguardBottomArea.getLockIcon().animate()
1083                    .alpha(0f)
1084                    .scaleX(2f)
1085                    .scaleY(2f)
1086                    .setInterpolator(mFastOutLinearInterpolator)
1087                    .setDuration(100);
1088        }
1089    }
1090
1091    @Override
1092    public void onHeightChanged(ExpandableView view) {
1093
1094        // Block update if we are in quick settings and just the top padding changed
1095        // (i.e. view == null).
1096        if (view == null && mQsExpanded) {
1097            return;
1098        }
1099        requestPanelHeightUpdate();
1100    }
1101
1102    @Override
1103    public void onScrollChanged() {
1104        if (mQsExpanded) {
1105            requestScrollerTopPaddingUpdate(false /* animate */);
1106            requestPanelHeightUpdate();
1107        }
1108    }
1109
1110    @Override
1111    protected void onConfigurationChanged(Configuration newConfig) {
1112        super.onConfigurationChanged(newConfig);
1113        mPageSwiper.onConfigurationChanged();
1114    }
1115
1116    @Override
1117    public void onClick(View v) {
1118        if (v == mHeader.getBackgroundView()) {
1119            onQsExpansionStarted();
1120            if (mQsExpanded) {
1121                flingSettings(0 /* vel */, false /* expand */);
1122            } else if (mQsExpansionEnabled) {
1123                flingSettings(0 /* vel */, true /* expand */);
1124            }
1125        }
1126    }
1127
1128    @Override
1129    public void onAnimationToSideStarted(boolean rightPage) {
1130        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
1131        if (start) {
1132            mKeyguardBottomArea.launchPhone();
1133        } else {
1134            mKeyguardBottomArea.launchCamera();
1135        }
1136        mBlockTouches = true;
1137    }
1138
1139    @Override
1140    protected void onEdgeClicked(boolean right) {
1141        if ((right && getRightIcon().getVisibility() != View.VISIBLE)
1142                || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) {
1143            return;
1144        }
1145        mHintAnimationRunning = true;
1146        mPageSwiper.startHintAnimation(right, new Runnable() {
1147            @Override
1148            public void run() {
1149                mHintAnimationRunning = false;
1150                mStatusBar.onHintFinished();
1151            }
1152        });
1153        startHighlightIconAnimation(right ? getRightIcon() : getLeftIcon());
1154        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right;
1155        if (start) {
1156            mStatusBar.onPhoneHintStarted();
1157        } else {
1158            mStatusBar.onCameraHintStarted();
1159        }
1160    }
1161
1162    @Override
1163    protected void startUnlockHintAnimation() {
1164        super.startUnlockHintAnimation();
1165        startHighlightIconAnimation(getCenterIcon());
1166    }
1167
1168    /**
1169     * Starts the highlight (making it fully opaque) animation on an icon.
1170     */
1171    private void startHighlightIconAnimation(final View icon) {
1172        icon.animate()
1173                .alpha(1.0f)
1174                .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION)
1175                .setInterpolator(mFastOutSlowInInterpolator)
1176                .withEndAction(new Runnable() {
1177                    @Override
1178                    public void run() {
1179                        icon.animate().alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT)
1180                                .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION)
1181                                .setInterpolator(mFastOutSlowInInterpolator);
1182                    }
1183                });
1184    }
1185
1186    @Override
1187    public float getPageWidth() {
1188        return getWidth();
1189    }
1190
1191    @Override
1192    public ArrayList<View> getTranslationViews() {
1193        return mSwipeTranslationViews;
1194    }
1195
1196    @Override
1197    public View getLeftIcon() {
1198        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1199                ? mKeyguardBottomArea.getCameraImageView()
1200                : mKeyguardBottomArea.getPhoneImageView();
1201    }
1202
1203    @Override
1204    public View getCenterIcon() {
1205        return mKeyguardBottomArea.getLockIcon();
1206    }
1207
1208    @Override
1209    public View getRightIcon() {
1210        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1211                ? mKeyguardBottomArea.getPhoneImageView()
1212                : mKeyguardBottomArea.getCameraImageView();
1213    }
1214
1215    @Override
1216    protected float getPeekHeight() {
1217        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1218            return mNotificationStackScroller.getPeekHeight();
1219        } else {
1220            return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR;
1221        }
1222    }
1223
1224    @Override
1225    protected float getCannedFlingDurationFactor() {
1226        if (mQsExpanded) {
1227            return 0.7f;
1228        } else {
1229            return 0.6f;
1230        }
1231    }
1232
1233    @Override
1234    protected boolean isTrackingBlocked() {
1235        return mConflictingQsExpansionGesture && mQsExpanded;
1236    }
1237
1238    public void notifyVisibleChildrenChanged() {
1239        if (mNotificationStackScroller.getNotGoneChildCount() != 0) {
1240            mReserveNotificationSpace.setVisibility(View.VISIBLE);
1241        } else {
1242            mReserveNotificationSpace.setVisibility(View.GONE);
1243        }
1244    }
1245
1246    public boolean isQsExpanded() {
1247        return mQsExpanded;
1248    }
1249}
1250