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