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