NotificationPanelView.java revision c640fafbcd7ba513130dcbf5762adbd77a9ced88
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 mQsExpansionFromOverscroll;
99    private boolean mQsExpansionEnabled = true;
100    private ValueAnimator mQsExpansionAnimator;
101    private FlingAnimationUtils mFlingAnimationUtils;
102    private int mStatusBarMinHeight;
103    private boolean mUnlockIconActive;
104    private int mNotificationsHeaderCollideDistance;
105    private int mUnlockMoveDistance;
106
107    private Interpolator mFastOutSlowInInterpolator;
108    private Interpolator mFastOutLinearInterpolator;
109    private Interpolator mLinearOutSlowInInterpolator;
110    private ObjectAnimator mClockAnimator;
111    private int mClockAnimationTarget = -1;
112    private int mTopPaddingAdjustment;
113    private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
114            new KeyguardClockPositionAlgorithm();
115    private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
116            new KeyguardClockPositionAlgorithm.Result();
117    private boolean mIsExpanding;
118
119    private boolean mBlockTouches;
120    private ArrayList<View> mSwipeTranslationViews = new ArrayList<>();
121    private int mNotificationScrimWaitDistance;
122    private boolean mOnNotificationsOnDown;
123
124    public NotificationPanelView(Context context, AttributeSet attrs) {
125        super(context, attrs);
126        mSystemIconsCopy = new MirrorView(context);
127    }
128
129    public void setStatusBar(PhoneStatusBar bar) {
130        mStatusBar = bar;
131    }
132
133    @Override
134    protected void onFinishInflate() {
135        super.onFinishInflate();
136        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
137        mHeader.getBackgroundView().setOnClickListener(this);
138        mHeader.setOverlayParent(this);
139        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
140        mQsContainer = findViewById(R.id.quick_settings_container);
141        mQsPanel = (QSPanel) findViewById(R.id.quick_settings_panel);
142        mClockView = (TextView) findViewById(R.id.clock_view);
143        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
144        mScrollView.setListener(this);
145        mNotificationStackScroller = (NotificationStackScrollLayout)
146                findViewById(R.id.notification_stack_scroller);
147        mNotificationStackScroller.setOnHeightChangedListener(this);
148        mNotificationStackScroller.setOverscrollTopChangedListener(this);
149        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
150                android.R.interpolator.fast_out_slow_in);
151        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
152                android.R.interpolator.linear_out_slow_in);
153        mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(),
154                android.R.interpolator.fast_out_linear_in);
155        mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area);
156        mSwipeTranslationViews.add(mNotificationStackScroller);
157        mSwipeTranslationViews.add(mKeyguardStatusView);
158        mPageSwiper = new KeyguardPageSwipeHelper(this, getContext());
159    }
160
161    @Override
162    protected void loadDimens() {
163        super.loadDimens();
164        mNotificationTopPadding = getResources().getDimensionPixelSize(
165                R.dimen.notifications_top_padding);
166        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
167        mStatusBarMinHeight = getResources().getDimensionPixelSize(
168                com.android.internal.R.dimen.status_bar_height);
169        mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
170        mNotificationsHeaderCollideDistance =
171                getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance);
172        mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance);
173        mClockPositionAlgorithm.loadDimens(getResources());
174        mNotificationScrimWaitDistance =
175                getResources().getDimensionPixelSize(R.dimen.notification_scrim_wait_distance);
176    }
177
178    @Override
179    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
180        super.onLayout(changed, left, top, right, bottom);
181
182        // Update Clock Pivot
183        mKeyguardStatusView.setPivotX(getWidth() / 2);
184        mKeyguardStatusView.setPivotY(
185                (FONT_HEIGHT - CAP_HEIGHT) / 2048f * mClockView.getTextSize());
186
187        // Calculate quick setting heights.
188        mQsMinExpansionHeight = mHeader.getCollapsedHeight() + mQsPeekHeight;
189        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
190        if (mQsExpanded) {
191            if (mQsFullyExpanded) {
192                mQsExpansionHeight = mQsMaxExpansionHeight;
193                requestScrollerTopPaddingUpdate(false /* animate */);
194            }
195        } else {
196            if (!mStackScrollerOverscrolling) {
197                setQsExpansion(mQsMinExpansionHeight);
198            }
199            positionClockAndNotifications();
200            mNotificationStackScroller.setStackHeight(getExpandedHeight());
201        }
202    }
203
204    /**
205     * Positions the clock and notifications dynamically depending on how many notifications are
206     * showing.
207     */
208    private void positionClockAndNotifications() {
209        boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
210        int stackScrollerPadding;
211        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
212            int bottom = mHeader.getCollapsedHeight();
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, boolean isRubberbanded) {
529        cancelAnimation();
530        float rounded = amount >= 1f ? amount : 0f;
531        mStackScrollerOverscrolling = rounded != 0f && isRubberbanded;
532        mQsExpansionFromOverscroll = rounded != 0f;
533        setQsExpansion(mQsMinExpansionHeight + rounded);
534        updateQsState();
535    }
536
537    @Override
538    public void flingTopOverscroll(float velocity, boolean open) {
539        setQsExpansion(mQsExpansionHeight);
540        flingSettings(velocity, open, new Runnable() {
541            @Override
542            public void run() {
543                mStackScrollerOverscrolling = false;
544                mQsExpansionFromOverscroll = false;
545            }
546        });
547    }
548
549    private void onQsExpansionStarted() {
550        onQsExpansionStarted(0);
551    }
552
553    private void onQsExpansionStarted(int overscrollAmount) {
554        cancelAnimation();
555
556        // Reset scroll position and apply that position to the expanded height.
557        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
558        mScrollView.scrollTo(0, 0);
559        setQsExpansion(height);
560    }
561
562    private void setQsExpanded(boolean expanded) {
563        boolean changed = mQsExpanded != expanded;
564        if (changed) {
565            mQsExpanded = expanded;
566            updateQsState();
567        }
568    }
569
570    public void setKeyguardShowing(boolean keyguardShowing) {
571        if (!mKeyguardShowing && keyguardShowing) {
572            setQsTranslation(mQsExpansionHeight);
573            mHeader.setTranslationY(0f);
574        }
575        mKeyguardShowing = keyguardShowing;
576        updateQsState();
577    }
578
579    private void updateQsState() {
580        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling;
581        mHeader.setExpanded(expandVisually, mStackScrollerOverscrolling);
582        mNotificationStackScroller.setEnabled(!mQsExpanded || mQsExpansionFromOverscroll);
583        mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
584        mQsContainer.setVisibility(
585                mKeyguardShowing && !expandVisually ? View.INVISIBLE : View.VISIBLE);
586        mScrollView.setTouchEnabled(mQsExpanded);
587        mNotificationStackScroller.setTouchEnabled(!mQsExpanded || mQsExpansionFromOverscroll);
588    }
589
590    private void setQsExpansion(float height) {
591        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
592        mQsFullyExpanded = height == mQsMaxExpansionHeight;
593        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
594            setQsExpanded(true);
595        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
596            setQsExpanded(false);
597        }
598        mQsExpansionHeight = height;
599        mHeader.setExpansion(height - mQsPeekHeight);
600        setQsTranslation(height);
601        requestScrollerTopPaddingUpdate(false /* animate */);
602        updateNotificationScrim(height);
603        mStatusBar.userActivity();
604    }
605
606    private void updateNotificationScrim(float height) {
607        int startDistance = mQsMinExpansionHeight + mNotificationScrimWaitDistance;
608        float progress = (height - startDistance) / (mQsMaxExpansionHeight - startDistance);
609        progress = Math.max(0.0f, Math.min(progress, 1.0f));
610        mNotificationStackScroller.setScrimAlpha(progress);
611    }
612
613    private void setQsTranslation(float height) {
614        mQsContainer.setY(height - mQsContainer.getHeight() + getHeaderTranslation());
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
651    private void flingSettings(float vel, boolean expand) {
652        flingSettings(vel, expand, null);
653    }
654
655    private void flingSettings(float vel, boolean expand, final Runnable onFinishRunnable) {
656        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
657        if (target == mQsExpansionHeight) {
658            if (onFinishRunnable != null) {
659                onFinishRunnable.run();
660            }
661            return;
662        }
663        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
664        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
665        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
666            @Override
667            public void onAnimationUpdate(ValueAnimator animation) {
668                setQsExpansion((Float) animation.getAnimatedValue());
669            }
670        });
671        animator.addListener(new AnimatorListenerAdapter() {
672            @Override
673            public void onAnimationEnd(Animator animation) {
674                mQsExpansionAnimator = null;
675                if (onFinishRunnable != null) {
676                    onFinishRunnable.run();
677                }
678            }
679        });
680        animator.start();
681        mQsExpansionAnimator = animator;
682    }
683
684    /**
685     * @return Whether we should intercept a gesture to open Quick Settings.
686     */
687    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
688        if (!mQsExpansionEnabled) {
689            return false;
690        }
691        boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight()
692                && y >= mHeader.getTop() && y <= mHeader.getBottom();
693        if (mQsExpanded) {
694            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
695        } else {
696            return onHeader;
697        }
698    }
699
700    @Override
701    public void setVisibility(int visibility) {
702        int oldVisibility = getVisibility();
703        super.setVisibility(visibility);
704        if (visibility != oldVisibility) {
705            reparentStatusIcons(visibility == VISIBLE);
706        }
707    }
708
709    /**
710     * When the notification panel gets expanded, we need to move the status icons in the header
711     * card.
712     */
713    private void reparentStatusIcons(boolean toHeader) {
714        if (mStatusBar == null) {
715            return;
716        }
717        LinearLayout systemIcons = mStatusBar.getSystemIcons();
718        ViewGroup parent = ((ViewGroup) systemIcons.getParent());
719        if (toHeader) {
720            int index = parent.indexOfChild(systemIcons);
721            parent.removeView(systemIcons);
722            mSystemIconsCopy.setMirroredView(
723                    systemIcons, systemIcons.getWidth(), systemIcons.getHeight());
724            parent.addView(mSystemIconsCopy, index);
725            mHeader.attachSystemIcons(systemIcons);
726        } else {
727            ViewGroup newParent = mStatusBar.getSystemIconArea();
728            int index = newParent.indexOfChild(mSystemIconsCopy);
729            parent.removeView(systemIcons);
730            mHeader.onSystemIconsDetached();
731            mSystemIconsCopy.setMirroredView(null, 0, 0);
732            newParent.removeView(mSystemIconsCopy);
733            newParent.addView(systemIcons, index);
734        }
735    }
736
737    @Override
738    protected boolean isScrolledToBottom() {
739        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
740            return true;
741        }
742        if (!isInSettings()) {
743            return mNotificationStackScroller.isScrolledToBottom();
744        }
745        return super.isScrolledToBottom();
746    }
747
748    @Override
749    protected int getMaxPanelHeight() {
750        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD
751                && mNotificationStackScroller.getNotGoneChildCount() == 0) {
752            return (int) ((mQsMinExpansionHeight + getOverExpansionAmount())
753                    * HEADER_RUBBERBAND_FACTOR);
754        }
755        // TODO: Figure out transition for collapsing when QS is open, adjust height here.
756        int emptyBottomMargin = mNotificationStackScroller.getEmptyBottomMargin();
757        int maxHeight = mNotificationStackScroller.getHeight() - emptyBottomMargin
758                - mTopPaddingAdjustment;
759        maxHeight = Math.max(maxHeight, mStatusBarMinHeight);
760        return maxHeight;
761    }
762
763    private boolean isInSettings() {
764        return mQsExpanded;
765    }
766
767    @Override
768    protected void onHeightUpdated(float expandedHeight) {
769        if (!mQsExpanded) {
770            positionClockAndNotifications();
771        }
772        mNotificationStackScroller.setStackHeight(expandedHeight);
773        updateHeader();
774        updateUnlockIcon();
775        updateNotificationTranslucency();
776    }
777
778    private void updateNotificationTranslucency() {
779        float alpha = (mNotificationStackScroller.getNotificationsTopY()
780                + mNotificationStackScroller.getItemHeight())
781                / (mQsMinExpansionHeight
782                        + mNotificationStackScroller.getItemHeight() / 2);
783        alpha = Math.max(0, Math.min(alpha, 1));
784        alpha = (float) Math.pow(alpha, 0.75);
785
786        // TODO: Draw a rect with DST_OUT over the notifications to achieve the same effect -
787        // this would be much more efficient.
788        mNotificationStackScroller.setAlpha(alpha);
789    }
790
791    @Override
792    protected float getOverExpansionAmount() {
793        return mNotificationStackScroller.getCurrentOverScrollAmount(true /* top */);
794    }
795
796    @Override
797    protected float getOverExpansionPixels() {
798        return mNotificationStackScroller.getCurrentOverScrolledPixels(true /* top */);
799    }
800
801    private void updateUnlockIcon() {
802        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
803                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
804            boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance;
805            if (active && !mUnlockIconActive && mTracking) {
806                mKeyguardBottomArea.getLockIcon().animate()
807                        .alpha(1f)
808                        .scaleY(LOCK_ICON_ACTIVE_SCALE)
809                        .scaleX(LOCK_ICON_ACTIVE_SCALE)
810                        .setInterpolator(mFastOutLinearInterpolator)
811                        .setDuration(150);
812            } else if (!active && mUnlockIconActive && mTracking) {
813                mKeyguardBottomArea.getLockIcon().animate()
814                        .alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT)
815                        .scaleY(1f)
816                        .scaleX(1f)
817                        .setInterpolator(mFastOutLinearInterpolator)
818                        .setDuration(150);
819            }
820            mUnlockIconActive = active;
821        }
822    }
823
824    /**
825     * Hides the header when notifications are colliding with it.
826     */
827    private void updateHeader() {
828        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
829                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
830            updateHeaderKeyguard();
831        } else {
832            updateHeaderShade();
833        }
834
835    }
836
837    private void updateHeaderShade() {
838        mHeader.setAlpha(1f);
839        mHeader.setTranslationY(getHeaderTranslation());
840        setQsTranslation(mQsExpansionHeight);
841    }
842
843    private float getHeaderTranslation() {
844        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
845                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
846            return 0;
847        }
848        if (mNotificationStackScroller.getNotGoneChildCount() == 0) {
849            if (mExpandedHeight / HEADER_RUBBERBAND_FACTOR >= mQsMinExpansionHeight) {
850                return 0;
851            } else {
852                return mExpandedHeight / HEADER_RUBBERBAND_FACTOR - mQsMinExpansionHeight;
853            }
854        }
855        return Math.min(0, mNotificationStackScroller.getTranslationY()) / HEADER_RUBBERBAND_FACTOR;
856    }
857
858    private void updateHeaderKeyguard() {
859        mHeader.setTranslationY(0f);
860        float alpha;
861        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
862
863            // When on Keyguard, we hide the header as soon as the top card of the notification
864            // stack scroller is close enough (collision distance) to the bottom of the header.
865            alpha = mNotificationStackScroller.getNotificationsTopY()
866                    /
867                    (mQsMinExpansionHeight + mNotificationsHeaderCollideDistance);
868
869        } else {
870
871            // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
872            // soon as we start translating the stack.
873            alpha = mNotificationStackScroller.getNotificationsTopY() / mQsMinExpansionHeight;
874        }
875        alpha = Math.max(0, Math.min(alpha, 1));
876        alpha = (float) Math.pow(alpha, 0.75);
877        mHeader.setAlpha(alpha);
878        mKeyguardBottomArea.setAlpha(alpha);
879        setQsTranslation(mQsExpansionHeight);
880    }
881
882    @Override
883    protected void onExpandingStarted() {
884        super.onExpandingStarted();
885        mNotificationStackScroller.onExpansionStarted();
886        mIsExpanding = true;
887    }
888
889    @Override
890    protected void onExpandingFinished() {
891        super.onExpandingFinished();
892        mNotificationStackScroller.onExpansionStopped();
893        mIsExpanding = false;
894        if (mExpandedHeight == 0f) {
895            mHeader.setListening(false);
896            mQsPanel.setListening(false);
897        } else {
898            mHeader.setListening(true);
899            mQsPanel.setListening(true);
900        }
901    }
902
903    @Override
904    public void instantExpand() {
905        super.instantExpand();
906        mHeader.setListening(true);
907        mQsPanel.setListening(true);
908    }
909
910    @Override
911    protected void setOverExpansion(float overExpansion, boolean isPixels) {
912        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
913            mNotificationStackScroller.setOnHeightChangedListener(null);
914            if (isPixels) {
915                mNotificationStackScroller.setOverScrolledPixels(
916                        overExpansion, true /* onTop */, false /* animate */);
917            } else {
918                mNotificationStackScroller.setOverScrollAmount(
919                        overExpansion, true /* onTop */, false /* animate */);
920            }
921            mNotificationStackScroller.setOnHeightChangedListener(this);
922        }
923    }
924
925    @Override
926    protected void onTrackingStarted() {
927        super.onTrackingStarted();
928        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
929                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
930            mPageSwiper.animateHideLeftRightIcon();
931        }
932    }
933
934    @Override
935    protected void onTrackingStopped(boolean expand) {
936        super.onTrackingStopped(expand);
937        if (expand) {
938            mNotificationStackScroller.setOverScrolledPixels(
939                    0.0f, true /* onTop */, true /* animate */);
940        }
941        if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
942                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
943            mPageSwiper.showAllIcons(true);
944        }
945        if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
946                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
947            mKeyguardBottomArea.getLockIcon().animate()
948                    .alpha(0f)
949                    .scaleX(2f)
950                    .scaleY(2f)
951                    .setInterpolator(mFastOutLinearInterpolator)
952                    .setDuration(100);
953        }
954    }
955
956    @Override
957    public void onHeightChanged(ExpandableView view) {
958        requestPanelHeightUpdate();
959    }
960
961    @Override
962    public void onScrollChanged() {
963        if (mQsExpanded) {
964            requestScrollerTopPaddingUpdate(false /* animate */);
965        }
966    }
967
968    @Override
969    protected void onConfigurationChanged(Configuration newConfig) {
970        super.onConfigurationChanged(newConfig);
971        mPageSwiper.onConfigurationChanged();
972    }
973
974    @Override
975    public void onClick(View v) {
976        if (v == mHeader.getBackgroundView()) {
977            onQsExpansionStarted();
978            if (mQsExpanded) {
979                flingSettings(0 /* vel */, false /* expand */);
980            } else if (mQsExpansionEnabled) {
981                flingSettings(0 /* vel */, true /* expand */);
982            }
983        }
984    }
985
986    @Override
987    public void onAnimationToSideStarted(boolean rightPage) {
988        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
989        if (start) {
990            mKeyguardBottomArea.launchPhone();
991        } else {
992            mKeyguardBottomArea.launchCamera();
993        }
994        mBlockTouches = true;
995    }
996
997    @Override
998    protected void onEdgeClicked(boolean right) {
999        if ((right && getRightIcon().getVisibility() != View.VISIBLE)
1000                || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) {
1001            return;
1002        }
1003        mHintAnimationRunning = true;
1004        mPageSwiper.startHintAnimation(right, new Runnable() {
1005            @Override
1006            public void run() {
1007                mHintAnimationRunning = false;
1008                mStatusBar.onHintFinished();
1009            }
1010        });
1011        startHighlightIconAnimation(right ? getRightIcon() : getLeftIcon());
1012        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right;
1013        if (start) {
1014            mStatusBar.onPhoneHintStarted();
1015        } else {
1016            mStatusBar.onCameraHintStarted();
1017        }
1018    }
1019
1020    @Override
1021    protected void startUnlockHintAnimation() {
1022        super.startUnlockHintAnimation();
1023        startHighlightIconAnimation(getCenterIcon());
1024    }
1025
1026    /**
1027     * Starts the highlight (making it fully opaque) animation on an icon.
1028     */
1029    private void startHighlightIconAnimation(final View icon) {
1030        icon.animate()
1031                .alpha(1.0f)
1032                .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION)
1033                .setInterpolator(mFastOutSlowInInterpolator)
1034                .withEndAction(new Runnable() {
1035                    @Override
1036                    public void run() {
1037                        icon.animate().alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT)
1038                                .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION)
1039                                .setInterpolator(mFastOutSlowInInterpolator);
1040                    }
1041                });
1042    }
1043
1044    @Override
1045    public float getPageWidth() {
1046        return getWidth();
1047    }
1048
1049    @Override
1050    public ArrayList<View> getTranslationViews() {
1051        return mSwipeTranslationViews;
1052    }
1053
1054    @Override
1055    public View getLeftIcon() {
1056        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1057                ? mKeyguardBottomArea.getCameraImageView()
1058                : mKeyguardBottomArea.getPhoneImageView();
1059    }
1060
1061    @Override
1062    public View getCenterIcon() {
1063        return mKeyguardBottomArea.getLockIcon();
1064    }
1065
1066    @Override
1067    public View getRightIcon() {
1068        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
1069                ? mKeyguardBottomArea.getPhoneImageView()
1070                : mKeyguardBottomArea.getCameraImageView();
1071    }
1072
1073    @Override
1074    protected float getPeekHeight() {
1075        if (mNotificationStackScroller.getNotGoneChildCount() > 0) {
1076            return mNotificationStackScroller.getPeekHeight();
1077        } else {
1078            return mQsMinExpansionHeight * HEADER_RUBBERBAND_FACTOR;
1079        }
1080    }
1081}
1082