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