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