NotificationPanelView.java revision 605f1902023d3c372769cbeb97840c87b910164b
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.MirrorView;
42import com.android.systemui.statusbar.StatusBarState;
43import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
44import com.android.systemui.statusbar.stack.StackStateAnimator;
45
46import java.util.ArrayList;
47
48public class NotificationPanelView extends PanelView implements
49        ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener,
50        View.OnClickListener, NotificationStackScrollLayout.OnOverscrollTopChangedListener,
51        KeyguardPageSwipeHelper.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 LOCK_ICON_ACTIVE_SCALE = 1.2f;
59
60    private KeyguardPageSwipeHelper mPageSwiper;
61    private StatusBarHeaderView mHeader;
62    private View mQsContainer;
63    private View mQsPanel;
64    private View mKeyguardStatusView;
65    private ObservableScrollView mScrollView;
66    private TextView mClockView;
67
68    private MirrorView mSystemIconsCopy;
69
70    private NotificationStackScrollLayout mNotificationStackScroller;
71    private int mNotificationTopPadding;
72    private boolean mAnimateNextTopPaddingChange;
73
74    private int mTrackingPointer;
75    private VelocityTracker mVelocityTracker;
76    private boolean mQsTracking;
77
78    /**
79     * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't
80     * intercepted yet.
81     */
82    private boolean mIntercepting;
83    private boolean mQsExpanded;
84    private boolean mQsFullyExpanded;
85    private boolean mKeyguardShowing;
86    private float mInitialHeightOnTouch;
87    private float mInitialTouchX;
88    private float mInitialTouchY;
89    private float mLastTouchX;
90    private float mLastTouchY;
91    private float mQsExpansionHeight;
92    private int mQsMinExpansionHeight;
93    private int mQsMaxExpansionHeight;
94    private int mQsPeekHeight;
95    private boolean mStackScrollerOverscrolling;
96    private boolean mQsExpansionEnabled = true;
97    private ValueAnimator mQsExpansionAnimator;
98    private FlingAnimationUtils mFlingAnimationUtils;
99    private int mStatusBarMinHeight;
100    private boolean mHeaderHidden;
101    private boolean mUnlockIconActive;
102    private int mNotificationsHeaderCollideDistance;
103    private int mUnlockMoveDistance;
104
105    private Interpolator mFastOutSlowInInterpolator;
106    private Interpolator mFastOutLinearInterpolator;
107    private Interpolator mLinearOutSlowInInterpolator;
108    private ObjectAnimator mClockAnimator;
109    private int mClockAnimationTarget = -1;
110    private int mTopPaddingAdjustment;
111    private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
112            new KeyguardClockPositionAlgorithm();
113    private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
114            new KeyguardClockPositionAlgorithm.Result();
115    private boolean mIsExpanding;
116
117    private boolean mBlockTouches;
118    private ArrayList<View> mSwipeTranslationViews = new ArrayList<>();
119    private int mNotificationScrimWaitDistance;
120    private boolean mOnNotificationsOnDown;
121
122    public NotificationPanelView(Context context, AttributeSet attrs) {
123        super(context, attrs);
124        mSystemIconsCopy = new MirrorView(context);
125    }
126
127    public void setStatusBar(PhoneStatusBar bar) {
128        mStatusBar = bar;
129    }
130
131    @Override
132    protected void onFinishInflate() {
133        super.onFinishInflate();
134        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
135        mHeader.getBackgroundView().setOnClickListener(this);
136        mHeader.setOverlayParent(this);
137        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
138        mQsContainer = findViewById(R.id.quick_settings_container);
139        mQsPanel = findViewById(R.id.quick_settings_panel);
140        mClockView = (TextView) findViewById(R.id.clock_view);
141        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
142        mScrollView.setListener(this);
143        mNotificationStackScroller = (NotificationStackScrollLayout)
144                findViewById(R.id.notification_stack_scroller);
145        mNotificationStackScroller.setOnHeightChangedListener(this);
146        mNotificationStackScroller.setOverscrollTopChangedListener(this);
147        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
148                android.R.interpolator.fast_out_slow_in);
149        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
150                android.R.interpolator.linear_out_slow_in);
151        mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(),
152                android.R.interpolator.fast_out_linear_in);
153        mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area);
154        mSwipeTranslationViews.add(mNotificationStackScroller);
155        mSwipeTranslationViews.add(mKeyguardStatusView);
156        mPageSwiper = new KeyguardPageSwipeHelper(this, getContext());
157    }
158
159    @Override
160    protected void loadDimens() {
161        super.loadDimens();
162        mNotificationTopPadding = getResources().getDimensionPixelSize(
163                R.dimen.notifications_top_padding);
164        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
165        mStatusBarMinHeight = getResources().getDimensionPixelSize(
166                com.android.internal.R.dimen.status_bar_height);
167        mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
168        mNotificationsHeaderCollideDistance =
169                getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance);
170        mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance);
171        mClockPositionAlgorithm.loadDimens(getResources());
172        mNotificationScrimWaitDistance =
173                getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance);
174    }
175
176    @Override
177    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
178        super.onLayout(changed, left, top, right, bottom);
179
180        // Update Clock Pivot
181        mKeyguardStatusView.setPivotX(getWidth() / 2);
182        mKeyguardStatusView.setPivotY(
183                (FONT_HEIGHT - CAP_HEIGHT) / 2048f * mClockView.getTextSize());
184
185        // Calculate quick setting heights.
186        mQsMinExpansionHeight = mHeader.getCollapsedHeight() + mQsPeekHeight;
187        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
188        if (mQsExpanded) {
189            if (mQsFullyExpanded) {
190                mQsExpansionHeight = mQsMaxExpansionHeight;
191                requestScrollerTopPaddingUpdate(false /* animate */);
192            }
193        } else {
194            if (!mStackScrollerOverscrolling) {
195                setQsExpansion(mQsMinExpansionHeight);
196            }
197            positionClockAndNotifications();
198            mNotificationStackScroller.setStackHeight(getExpandedHeight());
199        }
200    }
201
202    /**
203     * Positions the clock and notifications dynamically depending on how many notifications are
204     * showing.
205     */
206    private void positionClockAndNotifications() {
207        boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
208        int stackScrollerPadding;
209        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
210            int bottom = mStackScrollerOverscrolling
211                    ? mHeader.getCollapsedHeight()
212                    : mHeader.getBottom();
213            stackScrollerPadding = bottom + mQsPeekHeight
214                    + mNotificationTopPadding;
215            mTopPaddingAdjustment = 0;
216        } else {
217            mClockPositionAlgorithm.setup(
218                    mStatusBar.getMaxKeyguardNotifications(),
219                    getMaxPanelHeight(),
220                    getExpandedHeight(),
221                    mNotificationStackScroller.getNotGoneChildCount(),
222                    getHeight(),
223                    mKeyguardStatusView.getHeight());
224            mClockPositionAlgorithm.run(mClockPositionResult);
225            if (animate || mClockAnimator != null) {
226                startClockAnimation(mClockPositionResult.clockY);
227            } else {
228                mKeyguardStatusView.setY(mClockPositionResult.clockY);
229            }
230            updateClock(mClockPositionResult.clockAlpha, mClockPositionResult.clockScale);
231            stackScrollerPadding = mClockPositionResult.stackScrollerPadding;
232            mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
233        }
234        mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding);
235        requestScrollerTopPaddingUpdate(animate);
236    }
237
238    private void startClockAnimation(int y) {
239        if (mClockAnimationTarget == y) {
240            return;
241        }
242        mClockAnimationTarget = y;
243        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
244            @Override
245            public boolean onPreDraw() {
246                getViewTreeObserver().removeOnPreDrawListener(this);
247                if (mClockAnimator != null) {
248                    mClockAnimator.removeAllListeners();
249                    mClockAnimator.cancel();
250                }
251                mClockAnimator = ObjectAnimator
252                        .ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget);
253                mClockAnimator.setInterpolator(mFastOutSlowInInterpolator);
254                mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
255                mClockAnimator.addListener(new AnimatorListenerAdapter() {
256                    @Override
257                    public void onAnimationEnd(Animator animation) {
258                        mClockAnimator = null;
259                        mClockAnimationTarget = -1;
260                    }
261                });
262                mClockAnimator.start();
263                return true;
264            }
265        });
266    }
267
268    private void updateClock(float alpha, float scale) {
269        mKeyguardStatusView.setAlpha(alpha);
270        mKeyguardStatusView.setScaleX(scale);
271        mKeyguardStatusView.setScaleY(scale);
272    }
273
274    public void animateToFullShade() {
275        mAnimateNextTopPaddingChange = true;
276        mNotificationStackScroller.goToFullShade();
277        requestLayout();
278    }
279
280    /**
281     * @return Whether Quick Settings are currently expanded.
282     */
283    public boolean isQsExpanded() {
284        return mQsExpanded;
285    }
286
287    public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
288        mQsExpansionEnabled = qsExpansionEnabled;
289    }
290
291    @Override
292    public void resetViews() {
293        mBlockTouches = false;
294        mUnlockIconActive = false;
295        mPageSwiper.reset();
296        closeQs();
297        mNotificationStackScroller.setOverScrollAmount(0f, true /* onTop */, false /* animate */,
298                true /* cancelAnimators */);
299    }
300
301    public void closeQs() {
302        cancelAnimation();
303        setQsExpansion(mQsMinExpansionHeight);
304    }
305
306    public void openQs() {
307        cancelAnimation();
308        if (mQsExpansionEnabled) {
309            setQsExpansion(mQsMaxExpansionHeight);
310        }
311    }
312
313    @Override
314    public void fling(float vel, boolean always) {
315        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
316        if (gr != null) {
317            gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
318        }
319        super.fling(vel, always);
320    }
321
322    @Override
323    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
324        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
325            event.getText()
326                    .add(getContext().getString(R.string.accessibility_desc_notification_shade));
327            return true;
328        }
329
330        return super.dispatchPopulateAccessibilityEvent(event);
331    }
332
333    @Override
334    public boolean onInterceptTouchEvent(MotionEvent event) {
335        if (mBlockTouches) {
336            return false;
337        }
338        int pointerIndex = event.findPointerIndex(mTrackingPointer);
339        if (pointerIndex < 0) {
340            pointerIndex = 0;
341            mTrackingPointer = event.getPointerId(pointerIndex);
342        }
343        final float x = event.getX(pointerIndex);
344        final float y = event.getY(pointerIndex);
345
346        switch (event.getActionMasked()) {
347            case MotionEvent.ACTION_DOWN:
348                mIntercepting = true;
349                mInitialTouchY = y;
350                mInitialTouchX = x;
351                initVelocityTracker();
352                trackMovement(event);
353                mOnNotificationsOnDown = isOnNotifications(x, y);
354                if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
355                    getParent().requestDisallowInterceptTouchEvent(true);
356                }
357                break;
358            case MotionEvent.ACTION_POINTER_UP:
359                final int upPointer = event.getPointerId(event.getActionIndex());
360                if (mTrackingPointer == upPointer) {
361                    // gesture is ongoing, find a new pointer to track
362                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
363                    mTrackingPointer = event.getPointerId(newIndex);
364                    mInitialTouchX = event.getX(newIndex);
365                    mInitialTouchY = event.getY(newIndex);
366                }
367                break;
368
369            case MotionEvent.ACTION_MOVE:
370                final float h = y - mInitialTouchY;
371                trackMovement(event);
372                if (mQsTracking) {
373
374                    // Already tracking because onOverscrolled was called. We need to update here
375                    // so we don't stop for a frame until the next touch event gets handled in
376                    // onTouchEvent.
377                    setQsExpansion(h + mInitialHeightOnTouch);
378                    trackMovement(event);
379                    mIntercepting = false;
380                    return true;
381                }
382                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
383                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
384                    onQsExpansionStarted();
385                    mInitialHeightOnTouch = mQsExpansionHeight;
386                    mInitialTouchY = y;
387                    mInitialTouchX = x;
388                    mQsTracking = true;
389                    mIntercepting = false;
390                    mNotificationStackScroller.removeLongPressCallback();
391                    return true;
392                }
393                break;
394
395            case MotionEvent.ACTION_CANCEL:
396            case MotionEvent.ACTION_UP:
397                trackMovement(event);
398                if (mQsTracking) {
399                    flingQsWithCurrentVelocity();
400                    mQsTracking = false;
401                } else if (mQsFullyExpanded && mOnNotificationsOnDown) {
402                    flingSettings(0 /* vel */, false /* expand */);
403                }
404                mIntercepting = false;
405                break;
406        }
407        return !mQsExpanded && super.onInterceptTouchEvent(event);
408    }
409
410    private boolean isOnNotifications(float x, float y) {
411        return mNotificationStackScroller.getChildAtPosition(x, y) != null;
412    }
413
414    @Override
415    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
416
417        // Block request when interacting with the scroll view so we can still intercept the
418        // scrolling when QS is expanded.
419        if (mScrollView.isDispatchingTouchEvent()) {
420            return;
421        }
422        super.requestDisallowInterceptTouchEvent(disallowIntercept);
423    }
424
425    private void flingQsWithCurrentVelocity() {
426        float vel = getCurrentVelocity();
427
428        // TODO: Better logic whether we should expand or not.
429        flingSettings(vel, vel > 0);
430    }
431
432    @Override
433    public boolean onTouchEvent(MotionEvent event) {
434        if (mBlockTouches) {
435            return false;
436        }
437        // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
438        // implementation.
439        if ((!mIsExpanding || mHintAnimationRunning)
440                && !mQsExpanded
441                && mStatusBar.getBarState() != StatusBarState.SHADE) {
442            mPageSwiper.onTouchEvent(event);
443            if (mPageSwiper.isSwipingInProgress()) {
444                return true;
445            }
446        }
447        if (mQsTracking || mQsExpanded) {
448            return onQsTouch(event);
449        }
450
451        super.onTouchEvent(event);
452        return true;
453    }
454
455    @Override
456    protected boolean hasConflictingGestures() {
457        return mStatusBar.getBarState() != StatusBarState.SHADE;
458    }
459
460    private boolean onQsTouch(MotionEvent event) {
461        int pointerIndex = event.findPointerIndex(mTrackingPointer);
462        if (pointerIndex < 0) {
463            pointerIndex = 0;
464            mTrackingPointer = event.getPointerId(pointerIndex);
465        }
466        final float y = event.getY(pointerIndex);
467        final float x = event.getX(pointerIndex);
468
469        switch (event.getActionMasked()) {
470            case MotionEvent.ACTION_DOWN:
471                mQsTracking = true;
472                mInitialTouchY = y;
473                mInitialTouchX = x;
474                onQsExpansionStarted();
475                mInitialHeightOnTouch = mQsExpansionHeight;
476                initVelocityTracker();
477                trackMovement(event);
478                break;
479
480            case MotionEvent.ACTION_POINTER_UP:
481                final int upPointer = event.getPointerId(event.getActionIndex());
482                if (mTrackingPointer == upPointer) {
483                    // gesture is ongoing, find a new pointer to track
484                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
485                    final float newY = event.getY(newIndex);
486                    final float newX = event.getX(newIndex);
487                    mTrackingPointer = event.getPointerId(newIndex);
488                    mInitialHeightOnTouch = mQsExpansionHeight;
489                    mInitialTouchY = newY;
490                    mInitialTouchX = newX;
491                }
492                break;
493
494            case MotionEvent.ACTION_MOVE:
495                final float h = y - mInitialTouchY;
496                setQsExpansion(h + mInitialHeightOnTouch);
497                trackMovement(event);
498                break;
499
500            case MotionEvent.ACTION_UP:
501            case MotionEvent.ACTION_CANCEL:
502                mQsTracking = false;
503                mTrackingPointer = -1;
504                trackMovement(event);
505                flingQsWithCurrentVelocity();
506                if (mVelocityTracker != null) {
507                    mVelocityTracker.recycle();
508                    mVelocityTracker = null;
509                }
510                break;
511        }
512        return true;
513    }
514
515    @Override
516    public void onOverscrolled(int amount) {
517        if (mIntercepting) {
518            onQsExpansionStarted(amount);
519            mInitialHeightOnTouch = mQsExpansionHeight;
520            mInitialTouchY = mLastTouchY;
521            mInitialTouchX = mLastTouchX;
522            mQsTracking = true;
523        }
524    }
525
526
527    @Override
528    public void onOverscrollTopChanged(float amount) {
529        cancelAnimation();
530        float rounded = amount >= 1f ? amount : 0f;
531        mStackScrollerOverscrolling = rounded != 0f;
532        setQsExpansion(mQsMinExpansionHeight + rounded);
533        updateQsState();
534    }
535
536    @Override
537    public void flingTopOverscroll(float velocity, boolean open) {
538        mStackScrollerOverscrolling = false;
539        setQsExpansion(mQsExpansionHeight);
540        flingSettings(velocity, open);
541    }
542
543    private void onQsExpansionStarted() {
544        onQsExpansionStarted(0);
545    }
546
547    private void onQsExpansionStarted(int overscrollAmount) {
548        cancelAnimation();
549
550        // Reset scroll position and apply that position to the expanded height.
551        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
552        mScrollView.scrollTo(0, 0);
553        setQsExpansion(height);
554    }
555
556    private void setQsExpanded(boolean expanded) {
557        boolean changed = mQsExpanded != expanded;
558        if (changed) {
559            mQsExpanded = expanded;
560            updateQsState();
561        }
562    }
563
564    public void setKeyguardShowing(boolean keyguardShowing) {
565        mKeyguardShowing = keyguardShowing;
566        updateQsState();
567    }
568
569    private void updateQsState() {
570        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling;
571        mHeader.setExpanded(expandVisually, mStackScrollerOverscrolling);
572        mNotificationStackScroller.setEnabled(!mQsExpanded);
573        mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
574        mQsContainer.setVisibility(mKeyguardShowing && !expandVisually
575                ? View.INVISIBLE
576                : View.VISIBLE);
577        mScrollView.setTouchEnabled(mQsExpanded);
578        mNotificationStackScroller.setTouchEnabled(!mQsExpanded);
579    }
580
581    private void setQsExpansion(float height) {
582        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
583        mQsFullyExpanded = height == mQsMaxExpansionHeight;
584        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
585            setQsExpanded(true);
586        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
587            setQsExpanded(false);
588        }
589        mQsExpansionHeight = height;
590        mHeader.setExpansion(height - mQsPeekHeight);
591        setQsTranslation(height);
592        requestScrollerTopPaddingUpdate(false /* animate */);
593        updateNotificationScrim(height);
594        mStatusBar.userActivity();
595    }
596
597    private void updateNotificationScrim(float height) {
598        int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance;
599        float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance);
600        progress = Math.max(0.0f, Math.min(progress, 1.0f));
601        mNotificationStackScroller.setScrimAlpha(progress);
602    }
603
604    private void setQsTranslation(float height) {
605        mQsContainer.setY(height - mQsContainer.getHeight());
606    }
607
608
609    private void requestScrollerTopPaddingUpdate(boolean animate) {
610        mNotificationStackScroller.updateTopPadding(mQsExpansionHeight,
611                mScrollView.getScrollY(),
612                mAnimateNextTopPaddingChange || animate);
613        mAnimateNextTopPaddingChange = false;
614    }
615
616    private void trackMovement(MotionEvent event) {
617        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
618        mLastTouchX = event.getX();
619        mLastTouchY = event.getY();
620    }
621
622    private void initVelocityTracker() {
623        if (mVelocityTracker != null) {
624            mVelocityTracker.recycle();
625        }
626        mVelocityTracker = VelocityTracker.obtain();
627    }
628
629    private float getCurrentVelocity() {
630        if (mVelocityTracker == null) {
631            return 0;
632        }
633        mVelocityTracker.computeCurrentVelocity(1000);
634        return mVelocityTracker.getYVelocity();
635    }
636
637    private void cancelAnimation() {
638        if (mQsExpansionAnimator != null) {
639            mQsExpansionAnimator.cancel();
640        }
641    }
642    private void flingSettings(float vel, boolean expand) {
643        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
644        if (target == mQsExpansionHeight) {
645            return;
646        }
647        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
648        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
649        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
650            @Override
651            public void onAnimationUpdate(ValueAnimator animation) {
652                setQsExpansion((Float) animation.getAnimatedValue());
653            }
654        });
655        animator.addListener(new AnimatorListenerAdapter() {
656            @Override
657            public void onAnimationEnd(Animator animation) {
658                mQsExpansionAnimator = null;
659            }
660        });
661        animator.start();
662        mQsExpansionAnimator = animator;
663    }
664
665    /**
666     * @return Whether we should intercept a gesture to open Quick Settings.
667     */
668    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
669        if (!mQsExpansionEnabled) {
670            return false;
671        }
672        boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight()
673                && y >= mHeader.getTop() && y <= mHeader.getBottom();
674        if (mQsExpanded) {
675            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
676        } else {
677            return onHeader;
678        }
679    }
680
681    @Override
682    public void setVisibility(int visibility) {
683        int oldVisibility = getVisibility();
684        super.setVisibility(visibility);
685        if (visibility != oldVisibility) {
686            reparentStatusIcons(visibility == VISIBLE);
687        }
688    }
689
690    /**
691     * When the notification panel gets expanded, we need to move the status icons in the header
692     * card.
693     */
694    private void reparentStatusIcons(boolean toHeader) {
695        if (mStatusBar == null) {
696            return;
697        }
698        LinearLayout systemIcons = mStatusBar.getSystemIcons();
699        ViewGroup parent = ((ViewGroup) systemIcons.getParent());
700        if (toHeader) {
701            int index = parent.indexOfChild(systemIcons);
702            parent.removeView(systemIcons);
703            mSystemIconsCopy.setMirroredView(
704                    systemIcons, systemIcons.getWidth(), systemIcons.getHeight());
705            parent.addView(mSystemIconsCopy, index);
706            mHeader.attachSystemIcons(systemIcons);
707        } else {
708            ViewGroup newParent = mStatusBar.getSystemIconArea();
709            int index = newParent.indexOfChild(mSystemIconsCopy);
710            parent.removeView(systemIcons);
711            mHeader.onSystemIconsDetached();
712            mSystemIconsCopy.setMirroredView(null, 0, 0);
713            newParent.removeView(mSystemIconsCopy);
714            newParent.addView(systemIcons, index);
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