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