NotificationPanelView.java revision dbc3dce886cb7abba23a9f18e60d0f5af5c93226
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.PropertyValuesHolder;
23import android.animation.ValueAnimator;
24import android.content.Context;
25import android.content.res.Configuration;
26import android.util.AttributeSet;
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;
36import android.widget.TextView;
37
38import com.android.systemui.R;
39import com.android.systemui.qs.QSPanel;
40import com.android.systemui.statusbar.ExpandableView;
41import com.android.systemui.statusbar.FlingAnimationUtils;
42import com.android.systemui.statusbar.GestureRecorder;
43import com.android.systemui.statusbar.KeyguardAffordanceView;
44import com.android.systemui.statusbar.MirrorView;
45import com.android.systemui.statusbar.StatusBarState;
46import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
47import com.android.systemui.statusbar.stack.StackStateAnimator;
48
49public class NotificationPanelView extends PanelView implements
50        ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
51        View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
52        KeyguardAffordanceHelper.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 KeyguardAffordanceHelper mAfforanceHelper;
63    private StatusBarHeaderView mHeader;
64    private KeyguardStatusBarView mKeyguardStatusBar;
65    private View mQsContainer;
66    private QSPanel mQsPanel;
67    private View mKeyguardStatusView;
68    private ObservableScrollView mScrollView;
69    private TextView mClockView;
70    private View mReserveNotificationSpace;
71    private MirrorView mSystemIconsCopy;
72
73    private NotificationStackScrollLayout mNotificationStackScroller;
74    private int mNotificationTopPadding;
75    private boolean mAnimateNextTopPaddingChange;
76
77    private int mTrackingPointer;
78    private VelocityTracker mVelocityTracker;
79    private boolean mQsTracking;
80
81    /**
82     * If set, the ongoing touch gesture might both trigger the expansion in {@link PanelView} and
83     * the expansion for quick settings.
84     */
85    private boolean mConflictingQsExpansionGesture;
86
87    /**
88     * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't
89     * intercepted yet.
90     */
91    private boolean mIntercepting;
92    private boolean mQsExpanded;
93    private boolean mQsExpandedWhenExpandingStarted;
94    private boolean mQsFullyExpanded;
95    private boolean mKeyguardShowing;
96    private int mStatusBarState;
97    private float mInitialHeightOnTouch;
98    private float mInitialTouchX;
99    private float mInitialTouchY;
100    private float mLastTouchX;
101    private float mLastTouchY;
102    private float mQsExpansionHeight;
103    private int mQsMinExpansionHeight;
104    private int mQsMaxExpansionHeight;
105    private int mQsPeekHeight;
106    private boolean mStackScrollerOverscrolling;
107    private boolean mQsExpansionFromOverscroll;
108    private boolean mQsExpansionEnabled = true;
109    private ValueAnimator mQsExpansionAnimator;
110    private FlingAnimationUtils mFlingAnimationUtils;
111    private int mStatusBarMinHeight;
112    private boolean mUnlockIconActive;
113    private int mNotificationsHeaderCollideDistance;
114    private int mUnlockMoveDistance;
115    private float mEmptyDragAmount;
116
117    private Interpolator mFastOutSlowInInterpolator;
118    private Interpolator mFastOutLinearInterpolator;
119    private Interpolator mLinearOutSlowInInterpolator;
120    private ObjectAnimator mClockAnimator;
121    private int mClockAnimationTarget = -1;
122    private int mTopPaddingAdjustment;
123    private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
124            new KeyguardClockPositionAlgorithm();
125    private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
126            new KeyguardClockPositionAlgorithm.Result();
127    private boolean mIsExpanding;
128
129    private boolean mBlockTouches;
130    private int mNotificationScrimWaitDistance;
131    private boolean mTwoFingerQsExpand;
132    private boolean mTwoFingerQsExpandPossible;
133
134    /**
135     * If we are in a panel collapsing motion, we reset scrollY of our scroll view but still
136     * need to take this into account in our panel height calculation.
137     */
138    private int mScrollYOverride = -1;
139    private boolean mQsAnimatorExpand;
140    private boolean mIsLaunchTransitionFinished;
141    private boolean mIsLaunchTransitionRunning;
142    private Runnable mLaunchAnimationEndRunnable;
143    private boolean mOnlyAffordanceInThisMotion;
144    private boolean mKeyguardStatusViewAnimating;
145    private boolean mHeaderAnimatingIn;
146    private ObjectAnimator mQsContainerAnimator;
147
148    public NotificationPanelView(Context context, AttributeSet attrs) {
149        super(context, attrs);
150        mSystemIconsCopy = new MirrorView(context);
151    }
152
153    public void setStatusBar(PhoneStatusBar bar) {
154        mStatusBar = bar;
155    }
156
157    @Override
158    protected void onFinishInflate() {
159        super.onFinishInflate();
160        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
161        mHeader.setOnClickListener(this);
162        mKeyguardStatusBar = (KeyguardStatusBarView) findViewById(R.id.keyguard_header);
163        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
164        mQsContainer = findViewById(R.id.quick_settings_container);
165        mQsPanel = (QSPanel) findViewById(R.id.quick_settings_panel);
166        mClockView = (TextView) findViewById(R.id.clock_view);
167        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
168        mScrollView.setListener(this);
169        mReserveNotificationSpace = findViewById(R.id.reserve_notification_space);
170        mNotificationStackScroller = (NotificationStackScrollLayout)
171                findViewById(R.id.notification_stack_scroller);
172        mNotificationStackScroller.setOnHeightChangedListener(this);
173        mNotificationStackScroller.setOverscrollTopChangedListener(this);
174        mNotificationStackScroller.setScrollView(mScrollView);
175        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
176                android.R.interpolator.fast_out_slow_in);
177        mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(),
178                android.R.interpolator.fast_out_linear_in);
179        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
180                android.R.interpolator.linear_out_slow_in);
181        mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area);
182        mAfforanceHelper = new KeyguardAffordanceHelper(this, getContext());
183    }
184
185    @Override
186    protected void loadDimens() {
187        super.loadDimens();
188        mNotificationTopPadding = getResources().getDimensionPixelSize(
189                R.dimen.notifications_top_padding);
190        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
191        mStatusBarMinHeight = getResources().getDimensionPixelSize(
192                com.android.internal.R.dimen.status_bar_height);
193        mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
194        mNotificationsHeaderCollideDistance =
195                getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance);
196        mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance);
197        mClockPositionAlgorithm.loadDimens(getResources());
198        mNotificationScrimWaitDistance =
199                getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance);
200    }
201
202    @Override
203    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
204        super.onLayout(changed, left, top, right, bottom);
205
206        // Update Clock Pivot
207        mKeyguardStatusView.setPivotX(getWidth() / 2);
208        mKeyguardStatusView.setPivotY((FONT_HEIGHT - CAP_HEIGHT) / 2048f * mClockView.getTextSize());
209
210        // Calculate quick setting heights.
211        mQsMinExpansionHeight = mKeyguardShowing ? 0 : mHeader.getCollapsedHeight() + mQsPeekHeight;
212        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
213        if (mQsExpanded) {
214            if (mQsFullyExpanded) {
215                mQsExpansionHeight = mQsMaxExpansionHeight;
216                requestScrollerTopPaddingUpdate(false /* animate */);
217            }
218        } else {
219            if (!mStackScrollerOverscrolling) {
220                setQsExpansion(mQsMinExpansionHeight);
221            }
222            positionClockAndNotifications();
223            mNotificationStackScroller.setStackHeight(getExpandedHeight());
224        }
225    }
226
227    /**
228     * Positions the clock and notifications dynamically depending on how many notifications are
229     * showing.
230     */
231    private void positionClockAndNotifications() {
232        boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
233        int stackScrollerPadding;
234        if (mStatusBarState != StatusBarState.KEYGUARD) {
235            int bottom = mHeader.getCollapsedHeight();
236            stackScrollerPadding = mStatusBarState == StatusBarState.SHADE
237                    ? bottom + mQsPeekHeight + mNotificationTopPadding
238                    : mKeyguardStatusBar.getHeight() + mNotificationTopPadding;
239            mTopPaddingAdjustment = 0;
240        } else {
241            mClockPositionAlgorithm.setup(
242                    mStatusBar.getMaxKeyguardNotifications(),
243                    getMaxPanelHeight(),
244                    getExpandedHeight(),
245                    mNotificationStackScroller.getNotGoneChildCount(),
246                    getHeight(),
247                    mKeyguardStatusView.getHeight(),
248                    mEmptyDragAmount);
249            mClockPositionAlgorithm.run(mClockPositionResult);
250            if (animate || mClockAnimator != null) {
251                startClockAnimation(mClockPositionResult.clockY);
252            } else {
253                mKeyguardStatusView.setY(mClockPositionResult.clockY);
254            }
255            updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale);
256            stackScrollerPadding = mClockPositionResult.stackScrollerPadding;
257            mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
258        }
259        mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding);
260        requestScrollerTopPaddingUpdate(animate);
261    }
262
263    private void startClockAnimation(int y) {
264        if (mClockAnimationTarget == y) {
265            return;
266        }
267        mClockAnimationTarget = y;
268        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
269            @Override
270            public boolean onPreDraw() {
271                getViewTreeObserver().removeOnPreDrawListener(this);
272                if (mClockAnimator != null) {
273                    mClockAnimator.removeAllListeners();
274                    mClockAnimator.cancel();
275                }
276                mClockAnimator = ObjectAnimator
277                        .ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget);
278                mClockAnimator.setInterpolator(mFastOutSlowInInterpolator);
279                mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
280                mClockAnimator.addListener(new AnimatorListenerAdapter() {
281                    @Override
282                    public void onAnimationEnd(Animator animation) {
283                        mClockAnimator = null;
284                        mClockAnimationTarget = -1;
285                    }
286                });
287                mClockAnimator.start();
288                return true;
289            }
290        });
291    }
292
293    private void updateClock(float alpha, float scale) {
294        if (!mKeyguardStatusViewAnimating) {
295            mKeyguardStatusView.setAlpha(alpha);
296        }
297        mKeyguardStatusView.setScaleX(scale);
298        mKeyguardStatusView.setScaleY(scale);
299    }
300
301    public void animateToFullShade(long delay) {
302        mAnimateNextTopPaddingChange = true;
303        mNotificationStackScroller.goToFullShade(delay);
304        requestLayout();
305    }
306
307    public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
308        mQsExpansionEnabled = qsExpansionEnabled;
309    }
310
311    @Override
312    public void resetViews() {
313        mIsLaunchTransitionFinished = false;
314        mBlockTouches = false;
315        mUnlockIconActive = false;
316        mAfforanceHelper.reset(true);
317        closeQs();
318        mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */,
319                true /* cancelAnimators */);
320    }
321
322    public void closeQs() {
323        cancelAnimation();
324        setQsExpansion(mQsMinExpansionHeight);
325    }
326
327    public void animateCloseQs() {
328        if (mQsExpansionAnimator != null) {
329            if (!mQsAnimatorExpand) {
330                return;
331            }
332            float height = mQsExpansionHeight;
333            mQsExpansionAnimator.cancel();
334            setQsExpansion(height);
335        }
336        flingSettings(0 /* vel */, false);
337    }
338
339    public void openQs() {
340        cancelAnimation();
341        if (mQsExpansionEnabled) {
342            setQsExpansion(mQsMaxExpansionHeight);
343        }
344    }
345
346    @Override
347    public void fling(float vel, boolean always) {
348        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
349        if (gr != null) {
350            gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
351        }
352        super.fling(vel, always);
353    }
354
355    @Override
356    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
357        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
358            event.getText()
359                    .add(getContext().getString(R.string.accessibility_desc_notification_shade));
360            return true;
361        }
362
363        return super.dispatchPopulateAccessibilityEvent(event);
364    }
365
366    @Override
367    public boolean onInterceptTouchEvent(MotionEvent event) {
368        if (mBlockTouches) {
369            return false;
370        }
371        resetDownStates(event);
372        int pointerIndex = event.findPointerIndex(mTrackingPointer);
373        if (pointerIndex < 0) {
374            pointerIndex = 0;
375            mTrackingPointer = event.getPointerId(pointerIndex);
376        }
377        final float x = event.getX(pointerIndex);
378        final float y = event.getY(pointerIndex);
379
380        switch (event.getActionMasked()) {
381            case MotionEvent.ACTION_DOWN:
382                mIntercepting = true;
383                mInitialTouchY = y;
384                mInitialTouchX = x;
385                initVelocityTracker();
386                trackMovement(event);
387                if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
388                    getParent().requestDisallowInterceptTouchEvent(true);
389                }
390                if (mQsExpansionAnimator != null) {
391                    onQsExpansionStarted();
392                    mInitialHeightOnTouch = mQsExpansionHeight;
393                    mQsTracking = true;
394                    mIntercepting = false;
395                    mNotificationStackScroller.removeLongPressCallback();
396                }
397                break;
398            case MotionEvent.ACTION_POINTER_UP:
399                final int upPointer = event.getPointerId(event.getActionIndex());
400                if (mTrackingPointer == upPointer) {
401                    // gesture is ongoing, find a new pointer to track
402                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
403                    mTrackingPointer = event.getPointerId(newIndex);
404                    mInitialTouchX = event.getX(newIndex);
405                    mInitialTouchY = event.getY(newIndex);
406                }
407                break;
408
409            case MotionEvent.ACTION_MOVE:
410                final float h = y - mInitialTouchY;
411                trackMovement(event);
412                if (mQsTracking) {
413
414                    // Already tracking because onOverscrolled was called. We need to update here
415                    // so we don't stop for a frame until the next touch event gets handled in
416                    // onTouchEvent.
417                    setQsExpansion(h + mInitialHeightOnTouch);
418                    trackMovement(event);
419                    mIntercepting = false;
420                    return true;
421                }
422                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
423                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
424                    onQsExpansionStarted();
425                    mInitialHeightOnTouch = mQsExpansionHeight;
426                    mInitialTouchY = y;
427                    mInitialTouchX = x;
428                    mQsTracking = true;
429                    mIntercepting = false;
430                    mNotificationStackScroller.removeLongPressCallback();
431                    return true;
432                }
433                break;
434
435            case MotionEvent.ACTION_CANCEL:
436            case MotionEvent.ACTION_UP:
437                trackMovement(event);
438                if (mQsTracking) {
439                    flingQsWithCurrentVelocity();
440                    mQsTracking = false;
441                }
442                mIntercepting = false;
443                break;
444        }
445        return !mQsExpanded && super.onInterceptTouchEvent(event);
446    }
447
448    private void resetDownStates(MotionEvent event) {
449        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
450            mOnlyAffordanceInThisMotion = false;
451        }
452    }
453
454    @Override
455    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
456
457        // Block request when interacting with the scroll view so we can still intercept the
458        // scrolling when QS is expanded.
459        if (mScrollView.isHandlingTouchEvent()) {
460            return;
461        }
462        super.requestDisallowInterceptTouchEvent(disallowIntercept);
463    }
464
465    private void flingQsWithCurrentVelocity() {
466        float vel = getCurrentVelocity();
467        flingSettings(vel, flingExpandsQs(vel));
468    }
469
470    private boolean flingExpandsQs(float vel) {
471        if (Math.abs(vel) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
472            return getQsExpansionFraction() > 0.5f;
473        } else {
474            return vel > 0;
475        }
476    }
477
478    private float getQsExpansionFraction() {
479        return (mQsExpansionHeight - mQsMinExpansionHeight)
480                / (getTempQsMaxExpansion() - mQsMinExpansionHeight);
481    }
482
483    @Override
484    public boolean onTouchEvent(MotionEvent event) {
485        if (mBlockTouches) {
486            return false;
487        }
488        resetDownStates(event);
489        if ((!mIsExpanding || mHintAnimationRunning)
490                && !mQsExpanded
491                && mStatusBar.getBarState() != StatusBarState.SHADE) {
492            mAfforanceHelper.onTouchEvent(event);
493        }
494        if (mOnlyAffordanceInThisMotion) {
495            return true;
496        }
497        if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getExpandedFraction() == 1f
498                && mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
499
500            // Down in the empty area while fully expanded - go to QS.
501            mQsTracking = true;
502            mConflictingQsExpansionGesture = true;
503            onQsExpansionStarted();
504            mInitialHeightOnTouch = mQsExpansionHeight;
505            mInitialTouchY = event.getX();
506            mInitialTouchX = event.getY();
507        }
508        if (mExpandedHeight != 0) {
509            handleQsDown(event);
510        }
511        if (!mTwoFingerQsExpand && (mQsTracking || mQsExpanded)) {
512            onQsTouch(event);
513            if (!mConflictingQsExpansionGesture) {
514                return true;
515            }
516        }
517        if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
518                || event.getActionMasked() == MotionEvent.ACTION_UP) {
519            mConflictingQsExpansionGesture = false;
520        }
521        if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mExpandedHeight == 0) {
522            mTwoFingerQsExpandPossible = true;
523        }
524        if (mTwoFingerQsExpandPossible && event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN
525                && event.getPointerCount() == 2
526                && event.getY(event.getActionIndex()) < mStatusBarMinHeight) {
527            mTwoFingerQsExpand = true;
528            requestPanelHeightUpdate();
529        }
530        super.onTouchEvent(event);
531        return true;
532    }
533
534    private void handleQsDown(MotionEvent event) {
535        if (event.getActionMasked() == MotionEvent.ACTION_DOWN
536                && shouldQuickSettingsIntercept(event.getX(), event.getY(), 0)) {
537            mQsTracking = true;
538            onQsExpansionStarted();
539            mInitialHeightOnTouch = mQsExpansionHeight;
540            mInitialTouchY = event.getX();
541            mInitialTouchX = event.getY();
542        }
543    }
544
545    @Override
546    protected boolean flingExpands(float vel, float vectorVel) {
547        boolean expands = super.flingExpands(vel, vectorVel);
548
549        // If we are already running a QS expansion, make sure that we keep the panel open.
550        if (mQsExpansionAnimator != null) {
551            expands = true;
552        }
553        return expands;
554    }
555
556    @Override
557    protected boolean hasConflictingGestures() {
558        return mStatusBar.getBarState() != StatusBarState.SHADE;
559    }
560
561    private void onQsTouch(MotionEvent event) {
562        int pointerIndex = event.findPointerIndex(mTrackingPointer);
563        if (pointerIndex < 0) {
564            pointerIndex = 0;
565            mTrackingPointer = event.getPointerId(pointerIndex);
566        }
567        final float y = event.getY(pointerIndex);
568        final float x = event.getX(pointerIndex);
569
570        switch (event.getActionMasked()) {
571            case MotionEvent.ACTION_DOWN:
572                mQsTracking = true;
573                mInitialTouchY = y;
574                mInitialTouchX = x;
575                onQsExpansionStarted();
576                mInitialHeightOnTouch = mQsExpansionHeight;
577                initVelocityTracker();
578                trackMovement(event);
579                break;
580
581            case MotionEvent.ACTION_POINTER_UP:
582                final int upPointer = event.getPointerId(event.getActionIndex());
583                if (mTrackingPointer == upPointer) {
584                    // gesture is ongoing, find a new pointer to track
585                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
586                    final float newY = event.getY(newIndex);
587                    final float newX = event.getX(newIndex);
588                    mTrackingPointer = event.getPointerId(newIndex);
589                    mInitialHeightOnTouch = mQsExpansionHeight;
590                    mInitialTouchY = newY;
591                    mInitialTouchX = newX;
592                }
593                break;
594
595            case MotionEvent.ACTION_MOVE:
596                final float h = y - mInitialTouchY;
597                setQsExpansion(h + mInitialHeightOnTouch);
598                trackMovement(event);
599                break;
600
601            case MotionEvent.ACTION_UP:
602            case MotionEvent.ACTION_CANCEL:
603                mQsTracking = false;
604                mTrackingPointer = -1;
605                trackMovement(event);
606                float fraction = getQsExpansionFraction();
607                if ((fraction != 0f || y >= mInitialTouchY)
608                        && (fraction != 1f || y <= mInitialTouchY)) {
609                    flingQsWithCurrentVelocity();
610                } else {
611                    mScrollYOverride = -1;
612                }
613                if (mVelocityTracker != null) {
614                    mVelocityTracker.recycle();
615                    mVelocityTracker = null;
616                }
617                break;
618        }
619    }
620
621    @Override
622    public void onOverscrolled(int amount) {
623        if (mIntercepting) {
624            onQsExpansionStarted(amount);
625            mInitialHeightOnTouch = mQsExpansionHeight;
626            mInitialTouchY = mLastTouchY;
627            mInitialTouchX = mLastTouchX;
628            mQsTracking = true;
629        }
630    }
631
632    @Override
633    public void onOverscrollTopChanged(float amount, boolean isRubberbanded) {
634        cancelAnimation();
635        float rounded = amount >= 1f ? amount : 0f;
636        mStackScrollerOverscrolling = rounded != 0f && isRubberbanded;
637        mQsExpansionFromOverscroll = rounded != 0f;
638        updateQsState();
639        setQsExpansion(mQsMinExpansionHeight + rounded);
640    }
641
642    @Override
643    public void flingTopOverscroll(float velocity, boolean open) {
644        setQsExpansion(mQsExpansionHeight);
645        flingSettings(velocity, open, new Runnable() {
646            @Override
647            public void run() {
648                mStackScrollerOverscrolling = false;
649                mQsExpansionFromOverscroll = false;
650                updateQsState();
651            }
652        });
653    }
654
655    private void onQsExpansionStarted() {
656        onQsExpansionStarted(0);
657    }
658
659    private void onQsExpansionStarted(int overscrollAmount) {
660        cancelAnimation();
661
662        // Reset scroll position and apply that position to the expanded height.
663        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
664        if (mScrollView.getScrollY() != 0) {
665            mScrollYOverride = mScrollView.getScrollY();
666        }
667        mScrollView.scrollTo(0, 0);
668        setQsExpansion(height);
669    }
670
671    private void setQsExpanded(boolean expanded) {
672        boolean changed = mQsExpanded != expanded;
673        if (changed) {
674            mQsExpanded = expanded;
675            updateQsState();
676            requestPanelHeightUpdate();
677            mNotificationStackScroller.setInterceptDelegateEnabled(expanded);
678            mStatusBar.setQsExpanded(expanded);
679        }
680    }
681
682    public void setBarState(int statusBarState, boolean keyguardFadingAway,
683            boolean goingToFullShade) {
684        boolean keyguardShowing = statusBarState == StatusBarState.KEYGUARD
685                || statusBarState == StatusBarState.SHADE_LOCKED;
686        if (!mKeyguardShowing && keyguardShowing) {
687            setQsTranslation(mQsExpansionHeight);
688            mHeader.setTranslationY(0f);
689        }
690        setKeyguardStatusViewVisibility(statusBarState, keyguardFadingAway, goingToFullShade);
691        setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade);
692        if (goingToFullShade) {
693            animateKeyguardStatusBarOut();
694        } else {
695            mKeyguardStatusBar.setAlpha(1f);
696            mKeyguardStatusBar.setVisibility(keyguardShowing ? View.VISIBLE : View.INVISIBLE);
697        }
698        mStatusBarState = statusBarState;
699        mKeyguardShowing = keyguardShowing;
700        updateQsState();
701        if (goingToFullShade) {
702            animateHeaderSlidingIn();
703        }
704    }
705
706    private final Runnable mAnimateKeyguardStatusViewInvisibleEndRunnable = new Runnable() {
707        @Override
708        public void run() {
709            mKeyguardStatusViewAnimating = false;
710            mKeyguardStatusView.setVisibility(View.GONE);
711        }
712    };
713
714    private final Runnable mAnimateKeyguardStatusViewVisibleEndRunnable = new Runnable() {
715        @Override
716        public void run() {
717            mKeyguardStatusViewAnimating = false;
718        }
719    };
720
721    private final Animator.AnimatorListener mAnimateHeaderSlidingInListener
722            = new AnimatorListenerAdapter() {
723        @Override
724        public void onAnimationEnd(Animator animation) {
725            mHeaderAnimatingIn = false;
726            mQsContainerAnimator = null;
727            mQsContainer.removeOnLayoutChangeListener(mQsContainerAnimatorUpdater);
728        }
729    };
730
731    private final OnLayoutChangeListener mQsContainerAnimatorUpdater
732            = new OnLayoutChangeListener() {
733        @Override
734        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
735                int oldTop, int oldRight, int oldBottom) {
736            int oldHeight = oldBottom - oldTop;
737            int height = bottom - top;
738            if (height != oldHeight && mQsContainerAnimator != null) {
739                PropertyValuesHolder[] values = mQsContainerAnimator.getValues();
740                float newEndValue = mHeader.getCollapsedHeight() + mQsPeekHeight - height - top;
741                float newStartValue = -height - top;
742                values[0].setFloatValues(newStartValue, newEndValue);
743                mQsContainerAnimator.setCurrentPlayTime(mQsContainerAnimator.getCurrentPlayTime());
744            }
745        }
746    };
747
748    private final ViewTreeObserver.OnPreDrawListener mStartHeaderSlidingIn
749            = new ViewTreeObserver.OnPreDrawListener() {
750        @Override
751        public boolean onPreDraw() {
752            getViewTreeObserver().removeOnPreDrawListener(this);
753            mHeader.setTranslationY(-mHeader.getCollapsedHeight() - mQsPeekHeight);
754            mHeader.animate()
755                    .translationY(0f)
756                    .setStartDelay(mStatusBar.calculateGoingToFullShadeDelay())
757                    .setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE)
758                    .setInterpolator(mFastOutSlowInInterpolator)
759                    .start();
760            mQsContainer.setY(-mQsContainer.getHeight());
761            mQsContainerAnimator = ObjectAnimator.ofFloat(mQsContainer, View.TRANSLATION_Y,
762                    mQsContainer.getTranslationY(),
763                    mHeader.getCollapsedHeight() + mQsPeekHeight - mQsContainer.getHeight()
764                            - mQsContainer.getTop());
765            mQsContainerAnimator.setStartDelay(mStatusBar.calculateGoingToFullShadeDelay());
766            mQsContainerAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE);
767            mQsContainerAnimator.setInterpolator(mFastOutSlowInInterpolator);
768            mQsContainerAnimator.addListener(mAnimateHeaderSlidingInListener);
769            mQsContainerAnimator.start();
770            mQsContainer.addOnLayoutChangeListener(mQsContainerAnimatorUpdater);
771            return true;
772        }
773    };
774
775    private void animateHeaderSlidingIn() {
776        mHeaderAnimatingIn = true;
777        getViewTreeObserver().addOnPreDrawListener(mStartHeaderSlidingIn);
778
779    }
780
781    private final Runnable mAnimateKeyguardStatusBarInvisibleEndRunnable = new Runnable() {
782        @Override
783        public void run() {
784            mKeyguardStatusBar.setVisibility(View.INVISIBLE);
785        }
786    };
787
788    private void animateKeyguardStatusBarOut() {
789        mKeyguardStatusBar.animate()
790                .alpha(0f)
791                .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
792                .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
793                .setInterpolator(PhoneStatusBar.ALPHA_OUT)
794                .withEndAction(mAnimateKeyguardStatusBarInvisibleEndRunnable)
795                .start();
796    }
797
798    private final Runnable mAnimateKeyguardBottomAreaInvisibleEndRunnable = new Runnable() {
799        @Override
800        public void run() {
801            mKeyguardBottomArea.setVisibility(View.GONE);
802        }
803    };
804
805    private void setKeyguardBottomAreaVisibility(int statusBarState,
806            boolean goingToFullShade) {
807        if (goingToFullShade) {
808            mKeyguardBottomArea.animate().cancel();
809            mKeyguardBottomArea.animate()
810                    .alpha(0f)
811                    .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
812                    .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
813                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
814                    .withEndAction(mAnimateKeyguardBottomAreaInvisibleEndRunnable)
815                    .start();
816        } else if (statusBarState == StatusBarState.KEYGUARD
817                || statusBarState == StatusBarState.SHADE_LOCKED) {
818            mKeyguardBottomArea.animate().cancel();
819            mKeyguardBottomArea.setVisibility(View.VISIBLE);
820            mKeyguardBottomArea.setAlpha(1f);
821        } else {
822            mKeyguardBottomArea.animate().cancel();
823            mKeyguardBottomArea.setVisibility(View.GONE);
824            mKeyguardBottomArea.setAlpha(1f);
825        }
826    }
827
828    private void setKeyguardStatusViewVisibility(int statusBarState, boolean keyguardFadingAway,
829            boolean goingToFullShade) {
830        if ((!keyguardFadingAway && mStatusBarState == StatusBarState.KEYGUARD
831                && statusBarState != StatusBarState.KEYGUARD) || goingToFullShade) {
832            mKeyguardStatusView.animate().cancel();
833            mKeyguardStatusViewAnimating = true;
834            mKeyguardStatusView.animate()
835                    .alpha(0f)
836                    .setStartDelay(0)
837                    .setDuration(160)
838                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
839                    .withEndAction(mAnimateKeyguardStatusViewInvisibleEndRunnable);
840            if (keyguardFadingAway) {
841                mKeyguardStatusView.animate()
842                        .setStartDelay(mStatusBar.getKeyguardFadingAwayDelay())
843                        .setDuration(mStatusBar.getKeyguardFadingAwayDuration()/2)
844                        .start();
845            }
846        } else if (mStatusBarState == StatusBarState.SHADE_LOCKED
847                && statusBarState == StatusBarState.KEYGUARD) {
848            mKeyguardStatusView.animate().cancel();
849            mKeyguardStatusView.setVisibility(View.VISIBLE);
850            mKeyguardStatusViewAnimating = true;
851            mKeyguardStatusView.setAlpha(0f);
852            mKeyguardStatusView.animate()
853                    .alpha(1f)
854                    .setStartDelay(0)
855                    .setDuration(320)
856                    .setInterpolator(PhoneStatusBar.ALPHA_IN)
857                    .withEndAction(mAnimateKeyguardStatusViewVisibleEndRunnable);
858        } else if (statusBarState == StatusBarState.KEYGUARD) {
859            mKeyguardStatusView.animate().cancel();
860            mKeyguardStatusView.setVisibility(View.VISIBLE);
861            mKeyguardStatusView.setAlpha(1f);
862        } else {
863            mKeyguardStatusView.animate().cancel();
864            mKeyguardStatusView.setVisibility(View.GONE);
865            mKeyguardStatusView.setAlpha(1f);
866        }
867    }
868
869    private void updateQsState() {
870        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling;
871        mHeader.setVisibility((mQsExpanded || !mKeyguardShowing) ? View.VISIBLE : View.INVISIBLE);
872        mHeader.setExpanded(mKeyguardShowing || (mQsExpanded && !mStackScrollerOverscrolling));
873        mNotificationStackScroller.setScrollingEnabled(
874                mStatusBarState != StatusBarState.KEYGUARD && (!mQsExpanded
875                        || mQsExpansionFromOverscroll));
876        mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
877        mQsContainer.setVisibility(
878                mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE);
879        mScrollView.setTouchEnabled(mQsExpanded);
880    }
881
882    private void setQsExpansion(float height) {
883        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
884        mQsFullyExpanded = height == mQsMaxExpansionHeight;
885        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
886            setQsExpanded(true);
887        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
888            setQsExpanded(false);
889        }
890        mQsExpansionHeight = height;
891        mHeader.setExpansion(getHeaderExpansionFraction());
892        setQsTranslation(height);
893        requestScrollerTopPaddingUpdate(false /* animate */);
894        updateNotificationScrim(height);
895        if (mKeyguardShowing) {
896            float alpha = getQsExpansionFraction();
897            alpha *= 2;
898            alpha = Math.min(1, alpha);
899            alpha = 1 - alpha;
900            if (alpha == 0f) {
901                mKeyguardStatusBar.setVisibility(View.INVISIBLE);
902            } else {
903                mKeyguardStatusBar.setVisibility(View.VISIBLE);
904                mKeyguardStatusBar.setAlpha(alpha);
905            }
906        }
907    }
908
909    private void updateNotificationScrim(float height) {
910        int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance;
911        float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance);
912        progress = Math.max(0.0f, Math.min(progress, 1.0f));
913        mNotificationStackScroller.setScrimAlpha(progress);
914    }
915
916    private float getHeaderExpansionFraction() {
917        if (!mKeyguardShowing) {
918            return getQsExpansionFraction();
919        } else {
920            return 1f;
921        }
922    }
923
924    private void setQsTranslation(float height) {
925        if (!mHeaderAnimatingIn) {
926            mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation());
927        }
928        if (mKeyguardShowing) {
929            mHeader.setY(interpolate(getQsExpansionFraction(), -mHeader.getHeight(), 0));
930        }
931    }
932
933    private float calculateQsTopPadding() {
934        if (mKeyguardShowing) {
935            return interpolate(getQsExpansionFraction(),
936                    mNotificationStackScroller.getIntrinsicPadding() - mNotificationTopPadding,
937                    mQsMaxExpansionHeight);
938        } else {
939            return mQsExpansionHeight;
940        }
941    }
942
943    private void requestScrollerTopPaddingUpdate(boolean animate) {
944        mNotificationStackScroller.updateTopPadding(calculateQsTopPadding(),
945                mScrollView.getScrollY(),
946                mAnimateNextTopPaddingChange || animate);
947        mAnimateNextTopPaddingChange = false;
948    }
949
950    private void trackMovement(MotionEvent event) {
951        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
952        mLastTouchX = event.getX();
953        mLastTouchY = event.getY();
954    }
955
956    private void initVelocityTracker() {
957        if (mVelocityTracker != null) {
958            mVelocityTracker.recycle();
959        }
960        mVelocityTracker = VelocityTracker.obtain();
961    }
962
963    private float getCurrentVelocity() {
964        if (mVelocityTracker == null) {
965            return 0;
966        }
967        mVelocityTracker.computeCurrentVelocity(1000);
968        return mVelocityTracker.getYVelocity();
969    }
970
971    private void cancelAnimation() {
972        if (mQsExpansionAnimator != null) {
973            mQsExpansionAnimator.cancel();
974        }
975    }
976
977    private void flingSettings(float vel, boolean expand) {
978        flingSettings(vel, expand, null);
979    }
980
981    private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) {
982        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
983        if (target == mQsExpansionHeight) {
984            mScrollYOverride = -1;
985            if (onFinishRunnable != null) {
986                onFinishRunnable.run();
987            }
988            return;
989        }
990        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
991        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
992        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
993            @Override
994            public void onAnimationUpdate(ValueAnimator animation) {
995                setQsExpansion((Float) animation.getAnimatedValue());
996            }
997        });
998        animator.addListener(new AnimatorListenerAdapter() {
999            @Override
1000            public void onAnimationEnd(Animator animation) {
1001                mScrollYOverride = -1;
1002                mQsExpansionAnimator = null;
1003                if (onFinishRunnable != null) {
1004                    onFinishRunnable.run();
1005                }
1006            }
1007        });
1008        animator.start();
1009        mQsExpansionAnimator = animator;
1010        mQsAnimatorExpand = expand;
1011    }
1012
1013    /**
1014     * @return Whether we should intercept a gesture to open Quick Settings.
1015     */
1016    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
1017        if (!mQsExpansionEnabled) {
1018            return false;
1019        }
1020        View header = mKeyguardShowing ? mKeyguardStatusBar : mHeader;
1021        boolean onHeader = x >= header.getLeft() && x <= header.getRight()
1022                && y >= header.getTop() && y <= header.getBottom();
1023        if (mQsExpanded) {
1024            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
1025        } else {
1026            return onHeader;
1027        }
1028    }
1029
1030    @Override
1031    public void setVisibility(int visibility) {
1032        int oldVisibility = getVisibility();
1033        super.setVisibility(visibility);
1034        if (visibility != oldVisibility) {
1035            reparentStatusIcons(visibility == VISIBLE);
1036        }
1037    }
1038
1039    /**
1040     * When the notification panel gets expanded, we need to move the status icons in the header
1041     * card.
1042     */
1043    private void reparentStatusIcons(boolean toHeader) {
1044        if (mStatusBar == null) {
1045            return;
1046        }
1047        LinearLayout systemIcons = mStatusBar.getSystemIcons();
1048        ViewGroup parent = ((ViewGroup) systemIcons.getParent());
1049        if (toHeader) {
1050            int index = parent.indexOfChild(systemIcons);
1051            parent.removeView(systemIcons);
1052            mSystemIconsCopy.setMirroredView(
1053                    systemIcons, systemIcons.getWidth(), systemIcons.getHeight());
1054            parent.addView(mSystemIconsCopy, index);
1055            mHeader.attachSystemIcons(systemIcons);
1056        } else {
1057            ViewGroup newParent = mStatusBar.getSystemIconArea();
1058            int index = newParent.indexOfChild(mSystemIconsCopy);
1059            parent.removeView(systemIcons);
1060            mHeader.onSystemIconsDetached();
1061            mSystemIconsCopy.setMirroredView(null, 0, 0);
1062            newParent.removeView(mSystemIconsCopy);
1063            newParent.addView(systemIcons, index);
1064        }
1065    }
1066
1067    @Override
1068    protected boolean isScrolledToBottom() {
1069        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
1070            return true;
1071        }
1072        if (!isInSettings()) {
1073            return mNotificationStackScroller.isScrolledToBottom();
1074        }
1075        return super.isScrolledToBottom();
1076    }
1077
1078    @Override
1079    protected int getMaxPanelHeight() {
1080        int min = mStatusBarMinHeight;
1081        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD
1082                && mNotificationStackScroller.getNotGoneChildCount() == 0) {
1083            int minHeight = (int) ((mQsMinExpansionHeight + getOverExpansionAmount())
1084                    * HEADER_RUBBERBAND_FACTOR);
1085            min = Math.max(min, minHeight);
1086        }
1087        int maxHeight;
1088        if (mTwoFingerQsExpand || mQsExpanded || mIsExpanding && mQsExpandedWhenExpandingStarted) {
1089            maxHeight = Math.max(calculatePanelHeightQsExpanded(), calculatePanelHeightShade());
1090        } else {
1091            maxHeight = calculatePanelHeightShade();
1092        }
1093        maxHeight = Math.max(maxHeight, min);
1094        return maxHeight;
1095    }
1096
1097    private boolean isInSettings() {
1098        return mQsExpanded;
1099    }
1100
1101    @Override
1102    protected void onHeightUpdated(float expandedHeight) {
1103        if (!mQsExpanded) {
1104            positionClockAndNotifications();
1105        }
1106        if (mTwoFingerQsExpand || mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
1107                && !mQsExpansionFromOverscroll) {
1108            float panelHeightQsCollapsed = mNotificationStackScroller.getIntrinsicPadding()
1109                    + mNotificationStackScroller.getMinStackHeight()
1110                    + mNotificationStackScroller.getNotificationTopPadding();
1111            float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
1112            float t = (expandedHeight - panelHeightQsCollapsed)
1113                    / (panelHeightQsExpanded - panelHeightQsCollapsed);
1114
1115            setQsExpansion(mQsMinExpansionHeight
1116                    + t * (getTempQsMaxExpansion() - mQsMinExpansionHeight));
1117        }
1118        mNotificationStackScroller.setStackHeight(expandedHeight);
1119        updateHeader();
1120        updateUnlockIcon();
1121        updateNotificationTranslucency();
1122    }
1123
1124    /**
1125     * @return a temporary override of {@link #mQsMaxExpansionHeight}, which is needed when
1126     *         collapsing QS / the panel when QS was scrolled
1127     */
1128    private int getTempQsMaxExpansion() {
1129        int qsTempMaxExpansion = mQsMaxExpansionHeight;
1130        if (mScrollYOverride != -1) {
1131            qsTempMaxExpansion -= mScrollYOverride;
1132        }
1133        return qsTempMaxExpansion;
1134    }
1135
1136    private int calculatePanelHeightShade() {
1137        int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin();
1138        int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin
1139                - mTopPaddingAdjustment;
1140        maxHeight += mNotificationStackScroller.getTopPaddingOverflow();
1141        return maxHeight;
1142    }
1143
1144    private int calculatePanelHeightQsExpanded() {
1145        float notificationHeight = mNotificationStackScroller.getHeight()
1146                - mNotificationStackScroller.getEmptyBottomMargin()
1147                - mNotificationStackScroller.getTopPadding();
1148        float totalHeight = mQsMaxExpansionHeight + notificationHeight
1149                + mNotificationStackScroller.getNotificationTopPadding();
1150        if (totalHeight > mNotificationStackScroller.getHeight()) {
1151            float fullyCollapsedHeight = mQsMaxExpansionHeight
1152                    + mNotificationStackScroller.getMinStackHeight()
1153                    + mNotificationStackScroller.getNotificationTopPadding()
1154                    - getScrollViewScrollY();
1155            totalHeight = Math.max(fullyCollapsedHeight, mNotificationStackScroller.getHeight());
1156        }
1157        return (int) totalHeight;
1158    }
1159
1160    private int getScrollViewScrollY() {
1161        if (mScrollYOverride != -1) {
1162            return mScrollYOverride;
1163        } else {
1164            return mScrollView.getScrollY();
1165        }
1166    }
1167    private void updateNotificationTranslucency() {
1168        float alpha = (getNotificationsTopY() + mNotificationStackScroller.getItemHeight())
1169                / (mQsMinExpansionHeight + mNotificationStackScroller.getBottomStackPeekSize()
1170                        + mNotificationStackScroller.getCollapseSecondCardPadding());
1171        alpha = Math.max(0, Math.min(alpha, 1));
1172        alpha = (float) Math.pow(alpha, 0.75);
1173        if (alpha != 1f && mNotificationStackScroller.getLayerType() != LAYER_TYPE_HARDWARE) {
1174            mNotificationStackScroller.setLayerType(LAYER_TYPE_HARDWARE, null);
1175        } else if (alpha == 1f
1176                && mNotificationStackScroller.getLayerType() == LAYER_TYPE_HARDWARE) {
1177            mNotificationStackScroller.setLayerType(LAYER_TYPE_NONE, null);
1178        }
1179        mNotificationStackScroller.setAlpha(alpha);
1180    }
1181
1182    @Override
1183    protected float getOverExpansionAmount() {
1184        return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */);
1185    }
1186
1187    @Override
1188    protected float getOverExpansionPixels() {
1189        return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */);
1190    }
1191
1192    private void updateUnlockIcon() {
1193        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1194                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1195            boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance;
1196            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
1197            if (active && !mUnlockIconActive && mTracking) {
1198                lockIcon.setImageAlpha(1.0f, true, 150, mFastOutLinearInterpolator, null);
1199                lockIcon.setImageScale(LOCK_ICON_ACTIVE_SCALE, true, 150,
1200                        mFastOutLinearInterpolator);
1201            } else if (!active && mUnlockIconActive && mTracking) {
1202                lockIcon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT, true,
1203                        150, mFastOutLinearInterpolator, null);
1204                lockIcon.setImageScale(1.0f, true, 150,
1205                        mFastOutLinearInterpolator);
1206            }
1207            mUnlockIconActive = active;
1208        }
1209    }
1210
1211    /**
1212     * Hides the header when notifications are colliding with it.
1213     */
1214    private void updateHeader() {
1215        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1216                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1217            updateHeaderKeyguard();
1218        } else {
1219            updateHeaderShade();
1220        }
1221
1222    }
1223
1224    private void updateHeaderShade() {
1225        if (!mHeaderAnimatingIn) {
1226            mHeader.setTranslationY(getHeaderTranslation());
1227        }
1228        setQsTranslation(mQsExpansionHeight);
1229    }
1230
1231    private float getHeaderTranslation() {
1232        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1233                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1234            return 0;
1235        }
1236        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
1237            if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) {
1238                return 0;
1239            } else {
1240                return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight;
1241            }
1242        }
1243        return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR;
1244    }
1245
1246    private void updateHeaderKeyguard() {
1247        float alpha;
1248        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
1249
1250            // When on Keyguard, we hide the header as soon as the top card of the notification
1251            // stack scroller is close enough (collision distance) to the bottom of the header.
1252            alpha = getNotificationsTopY()
1253                    /
1254                    (mQsMinExpansionHeight + mNotificationsHeaderCollideDistance);
1255
1256        } else {
1257
1258            // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
1259            // soon as we start translating the stack.
1260            alpha = getNotificationsTopY() / mQsMinExpansionHeight;
1261        }
1262        alpha = Math.max(0, Math.min(alpha, 1));
1263        alpha = (float) Math.pow(alpha, 0.75);
1264        if (!mQsExpanded) {
1265            mKeyguardStatusBar.setAlpha(alpha);
1266        }
1267        mKeyguardBottomArea.setAlpha(alpha);
1268        setQsTranslation(mQsExpansionHeight);
1269    }
1270
1271    private float getNotificationsTopY() {
1272        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
1273            return getExpandedHeight();
1274        }
1275        return mNotificationStackScroller.getNotificationsTopY();
1276    }
1277
1278    @Override
1279    protected void onExpandingStarted() {
1280        super.onExpandingStarted();
1281        mNotificationStackScroller.onExpansionStarted();
1282        mIsExpanding = true;
1283        mQsExpandedWhenExpandingStarted = mQsExpanded;
1284        if (mQsExpanded) {
1285            onQsExpansionStarted();
1286        }
1287    }
1288
1289    @Override
1290    protected void onExpandingFinished() {
1291        super.onExpandingFinished();
1292        mNotificationStackScroller.onExpansionStopped();
1293        mIsExpanding = false;
1294        mScrollYOverride = -1;
1295        if (mExpandedHeight == 0f) {
1296            setListening(false);
1297        } else {
1298            setListening(true);
1299        }
1300        mTwoFingerQsExpand = false;
1301        mTwoFingerQsExpandPossible = false;
1302    }
1303
1304    private void setListening(boolean listening) {
1305        mHeader.setListening(listening);
1306        mKeyguardStatusBar.setListening(listening);
1307        mQsPanel.setListening(listening);
1308    }
1309
1310    @Override
1311    public void instantExpand() {
1312        super.instantExpand();
1313        setListening(true);
1314    }
1315
1316    @Override
1317    protected void setOverExpansion(float overExpansion, boolean isPixels) {
1318        if (mConflictingQsExpansionGesture || mTwoFingerQsExpand) {
1319            return;
1320        }
1321        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
1322            mNotificationStackScroller.setOnHeightChangedListener(null);
1323            if (isPixels) {
1324                mNotificationStackScroller.setOverScrolledPixels(
1325                        overExpansion, true /* onTop */, false /* animate */);
1326            } else {
1327                mNotificationStackScroller.setOverScrollAmount(
1328                        overExpansion, true /* onTop */, false /* animate */);
1329            }
1330            mNotificationStackScroller.setOnHeightChangedListener(this);
1331        }
1332    }
1333
1334    @Override
1335    protected void onTrackingStarted() {
1336        super.onTrackingStarted();
1337        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1338                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
1339            mAfforanceHelper.animateHideLeftRightIcon();
1340        }
1341    }
1342
1343    @Override
1344    protected void onTrackingStopped(boolean expand) {
1345        super.onTrackingStopped(expand);
1346        if (expand) {
1347            mNotificationStackScroller.setOverScrolledPixels(
1348                    0.0f, true /* onTop */, true /* animate */);
1349        }
1350        if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1351                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1352            if (!mHintAnimationRunning) {
1353                mAfforanceHelper.reset(true);
1354            }
1355        }
1356        if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
1357                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
1358            KeyguardAffordanceView lockIcon = mKeyguardBottomArea.getLockIcon();
1359            lockIcon.setImageAlpha(0.0f, true, 100, mFastOutLinearInterpolator, null);
1360            lockIcon.setImageScale(2.0f, true, 100, mFastOutLinearInterpolator);
1361        }
1362    }
1363
1364    @Override
1365    public void onHeightChanged(ExpandableView view) {
1366
1367        // Block update if we are in quick settings and just the top padding changed
1368        // (i.e. view == null).
1369        if (view == null && mQsExpanded) {
1370            return;
1371        }
1372        requestPanelHeightUpdate();
1373    }
1374
1375    @Override
1376    public void onScrollChanged() {
1377        if (mQsExpanded) {
1378            requestScrollerTopPaddingUpdate(false /* animate */);
1379            requestPanelHeightUpdate();
1380        }
1381    }
1382
1383    @Override
1384    protected void onConfigurationChanged(Configuration newConfig) {
1385        super.onConfigurationChanged(newConfig);
1386        mAfforanceHelper.onConfigurationChanged();
1387    }
1388
1389    @Override
1390    public void onClick(View v) {
1391        if (v == mHeader) {
1392            onQsExpansionStarted();
1393            if (mQsExpanded) {
1394                flingSettings(0 /* vel */, false /* expand */);
1395            } else if (mQsExpansionEnabled) {
1396                flingSettings(0 /* vel */, true /* expand */);
1397            }
1398        }
1399    }
1400
1401    @Override
1402    public void onAnimationToSideStarted(boolean rightPage) {
1403        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
1404        mIsLaunchTransitionRunning = true;
1405        mLaunchAnimationEndRunnable = null;
1406        if (start) {
1407            mKeyguardBottomArea.launchPhone();
1408        } else {
1409            mKeyguardBottomArea.launchCamera();
1410        }
1411        mBlockTouches = true;
1412    }
1413
1414    @Override
1415    public void onAnimationToSideEnded() {
1416        mIsLaunchTransitionRunning = false;
1417        mIsLaunchTransitionFinished = true;
1418        if (mLaunchAnimationEndRunnable != null) {
1419            mLaunchAnimationEndRunnable.run();
1420            mLaunchAnimationEndRunnable = null;
1421        }
1422    }
1423
1424    @Override
1425    protected void onEdgeClicked(boolean right) {
1426        if ((right && getRightIcon().getVisibility() != View.VISIBLE)
1427                || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) {
1428            return;
1429        }
1430        mHintAnimationRunning = true;
1431        mAfforanceHelper.startHintAnimation(right, new Runnable() {
1432            @Override
1433            public void run() {
1434                mHintAnimationRunning = false;
1435                mStatusBar.onHintFinished();
1436            }
1437        });
1438        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right;
1439        if (start) {
1440            mStatusBar.onPhoneHintStarted();
1441        } else {
1442            mStatusBar.onCameraHintStarted();
1443        }
1444    }
1445
1446    @Override
1447    protected void startUnlockHintAnimation() {
1448        super.startUnlockHintAnimation();
1449        startHighlightIconAnimation(getCenterIcon());
1450    }
1451
1452    /**
1453     * Starts the highlight (making it fully opaque) animation on an icon.
1454     */
1455    private void startHighlightIconAnimation(final KeyguardAffordanceView icon) {
1456        icon.setImageAlpha(1.0f, true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
1457                mFastOutSlowInInterpolator, new Runnable() {
1458                    @Override
1459                    public void run() {
1460                        icon.setImageAlpha(KeyguardAffordanceHelper.SWIPE_RESTING_ALPHA_AMOUNT,
1461                                true, KeyguardAffordanceHelper.HINT_PHASE1_DURATION,
1462                                mFastOutSlowInInterpolator, null);
1463                    }
1464                });
1465    }
1466
1467    @Override
1468    public float getPageWidth() {
1469        return getWidth();
1470    }
1471
1472    @Override
1473    public void onSwipingStarted() {
1474        requestDisallowInterceptTouchEvent(true);
1475        mOnlyAffordanceInThisMotion = true;
1476    }
1477
1478    @Override
1479    public KeyguardAffordanceView getLeftIcon() {
1480        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1481                ? mKeyguardBottomArea.getCameraView()
1482                : mKeyguardBottomArea.getPhoneView();
1483    }
1484
1485    @Override
1486    public KeyguardAffordanceView getCenterIcon() {
1487        return mKeyguardBottomArea.getLockIcon();
1488    }
1489
1490    @Override
1491    public KeyguardAffordanceView getRightIcon() {
1492        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1493                ? mKeyguardBottomArea.getPhoneView()
1494                : mKeyguardBottomArea.getCameraView();
1495    }
1496
1497    @Override
1498    public View getLeftPreview() {
1499        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1500                ? mKeyguardBottomArea.getCameraPreview()
1501                : mKeyguardBottomArea.getPhonePreview();
1502    }
1503
1504    @Override
1505    public View getRightPreview() {
1506        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1507                ? mKeyguardBottomArea.getPhonePreview()
1508                : mKeyguardBottomArea.getCameraPreview();
1509    }
1510
1511    @Override
1512    protected float getPeekHeight() {
1513        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1514            return mNotificationStackScroller.getPeekHeight();
1515        } else {
1516            return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR;
1517        }
1518    }
1519
1520    @Override
1521    protected float getCannedFlingDurationFactor() {
1522        if (mQsExpanded) {
1523            return 0.7f;
1524        } else {
1525            return 0.6f;
1526        }
1527    }
1528
1529    @Override
1530    protected boolean fullyExpandedClearAllVisible() {
1531        return mNotificationStackScroller.isDismissViewNotGone()
1532                && mNotificationStackScroller.isScrolledToBottom();
1533    }
1534
1535    @Override
1536    protected boolean isClearAllVisible() {
1537        return mNotificationStackScroller.isDismissViewVisible();
1538    }
1539
1540    @Override
1541    protected int getClearAllHeight() {
1542        return mNotificationStackScroller.getDismissViewHeight();
1543    }
1544
1545    @Override
1546    protected boolean isTrackingBlocked() {
1547        return mConflictingQsExpansionGesture && mQsExpanded;
1548    }
1549
1550    public void notifyVisibleChildrenChanged() {
1551        if (mNotificationStackScroller.getNotGoneChildCount() != 0) {
1552            mReserveNotificationSpace.setVisibility(View.VISIBLE);
1553        } else {
1554            mReserveNotificationSpace.setVisibility(View.GONE);
1555        }
1556    }
1557
1558    public boolean isQsExpanded() {
1559        return mQsExpanded;
1560    }
1561
1562    public boolean isQsDetailShowing() {
1563        return mQsPanel.isShowingDetail();
1564    }
1565
1566    public void closeQsDetail() {
1567        mQsPanel.closeDetail();
1568    }
1569
1570    @Override
1571    public boolean shouldDelayChildPressedState() {
1572        return true;
1573    }
1574
1575    public boolean isLaunchTransitionFinished() {
1576        return mIsLaunchTransitionFinished;
1577    }
1578
1579    public boolean isLaunchTransitionRunning() {
1580        return mIsLaunchTransitionRunning;
1581    }
1582
1583    public void setLaunchTransitionEndRunnable(Runnable r) {
1584        mLaunchAnimationEndRunnable = r;
1585    }
1586
1587    public void setEmptyDragAmount(float amount) {
1588        float factor = 1f;
1589        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1590            factor = 0.6f;
1591        } else if (!mStatusBar.hasActiveNotifications()) {
1592            factor = 0.4f;
1593        }
1594        mEmptyDragAmount = amount * factor;
1595        positionClockAndNotifications();
1596    }
1597
1598    private static float interpolate(float t, float start, float end) {
1599        return (1 - t) * start + t * end;
1600    }
1601}
1602