NotificationPanelView.java revision 4327e2fbcd68ec022a8d365bbe171dbc3cb97c65
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.util.Log;
27import android.view.MotionEvent;
28import android.view.VelocityTracker;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.ViewTreeObserver;
32import android.view.accessibility.AccessibilityEvent;
33import android.view.animation.AnimationUtils;
34import android.view.animation.Interpolator;
35import android.view.animation.LinearInterpolator;
36import android.widget.LinearLayout;
37
38import com.android.systemui.R;
39import com.android.systemui.statusbar.ExpandableView;
40import com.android.systemui.statusbar.FlingAnimationUtils;
41import com.android.systemui.statusbar.GestureRecorder;
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    private static float EXPANSION_RUBBER_BAND_EXTRA_FACTOR = 0.6f;
54
55    private KeyguardPageSwipeHelper mPageSwiper;
56    private StatusBarHeaderView mHeader;
57    private View mQsContainer;
58    private View mQsPanel;
59    private View mKeyguardStatusView;
60    private ObservableScrollView mScrollView;
61    private View mStackScrollerContainer;
62
63    private NotificationStackScrollLayout mNotificationStackScroller;
64    private int mNotificationTopPadding;
65    private boolean mAnimateNextTopPaddingChange;
66
67    private int mTrackingPointer;
68    private VelocityTracker mVelocityTracker;
69    private boolean mQsTracking;
70
71    /**
72     * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't
73     * intercepted yet.
74     */
75    private boolean mIntercepting;
76    private boolean mQsExpanded;
77    private boolean mQsFullyExpanded;
78    private boolean mKeyguardShowing;
79    private float mInitialHeightOnTouch;
80    private float mInitialTouchX;
81    private float mInitialTouchY;
82    private float mLastTouchX;
83    private float mLastTouchY;
84    private float mQsExpansionHeight;
85    private int mQsMinExpansionHeight;
86    private int mQsMaxExpansionHeight;
87    private int mMinStackHeight;
88    private int mQsPeekHeight;
89    private float mNotificationTranslation;
90    private int mStackScrollerIntrinsicPadding;
91    private boolean mStackScrollerOverscrolling;
92    private boolean mQsExpansionEnabled = true;
93    private ValueAnimator mQsExpansionAnimator;
94    private FlingAnimationUtils mFlingAnimationUtils;
95    private int mStatusBarMinHeight;
96    private boolean mHeaderHidden;
97    private int mNotificationsHeaderCollideDistance;
98
99    private Interpolator mFastOutSlowInInterpolator;
100    private Interpolator mFastOutLinearInterpolator;
101    private Interpolator mLinearOutSlowInInterpolator;
102    private ObjectAnimator mClockAnimator;
103    private int mClockAnimationTarget = -1;
104    private int mTopPaddingAdjustment;
105    private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
106            new KeyguardClockPositionAlgorithm();
107    private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
108            new KeyguardClockPositionAlgorithm.Result();
109    private boolean mIsSwipedHorizontally;
110    private boolean mIsExpanding;
111    private KeyguardBottomAreaView mKeyguardBottomArea;
112    private boolean mBlockTouches;
113    private ArrayList<View> mSwipeTranslationViews = new ArrayList<>();
114
115    public NotificationPanelView(Context context, AttributeSet attrs) {
116        super(context, attrs);
117    }
118
119    public void setStatusBar(PhoneStatusBar bar) {
120        if (mStatusBar != null) {
121            mStatusBar.setOnFlipRunnable(null);
122        }
123        mStatusBar = bar;
124        if (bar != null) {
125            mStatusBar.setOnFlipRunnable(new Runnable() {
126                @Override
127                public void run() {
128                    requestPanelHeightUpdate();
129                }
130            });
131        }
132    }
133
134    @Override
135    protected void onFinishInflate() {
136        super.onFinishInflate();
137        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
138        mHeader.getBackgroundView().setOnClickListener(this);
139        mHeader.setOverlayParent(this);
140        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
141        mStackScrollerContainer = findViewById(R.id.notification_container_parent);
142        mQsContainer = findViewById(R.id.quick_settings_container);
143        mQsPanel = findViewById(R.id.quick_settings_panel);
144        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
145        mScrollView.setListener(this);
146        mNotificationStackScroller = (NotificationStackScrollLayout)
147                findViewById(R.id.notification_stack_scroller);
148        mNotificationStackScroller.setOnHeightChangedListener(this);
149        mNotificationStackScroller.setOverscrollTopChangedListener(this);
150        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
151                android.R.interpolator.fast_out_slow_in);
152        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
153                android.R.interpolator.linear_out_slow_in);
154        mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(),
155                android.R.interpolator.fast_out_linear_in);
156        mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area);
157        mSwipeTranslationViews.add(mNotificationStackScroller);
158        mSwipeTranslationViews.add(mKeyguardStatusView);
159        mPageSwiper = new KeyguardPageSwipeHelper(this, getContext());
160    }
161
162    @Override
163    protected void loadDimens() {
164        super.loadDimens();
165        mNotificationTopPadding = getResources().getDimensionPixelSize(
166                R.dimen.notifications_top_padding);
167        mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height);
168        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
169        mStatusBarMinHeight = getResources().getDimensionPixelSize(
170                com.android.internal.R.dimen.status_bar_height);
171        mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
172        mNotificationsHeaderCollideDistance =
173                getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance);
174        mClockPositionAlgorithm.loadDimens(getResources());
175    }
176
177    @Override
178    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
179        super.onLayout(changed, left, top, right, bottom);
180
181        // Calculate quick setting heights.
182        mQsMinExpansionHeight = mHeader.getCollapsedHeight() + mQsPeekHeight;
183        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
184        if (mQsExpanded) {
185            if (mQsFullyExpanded) {
186                setQsStackScrollerPadding(mQsMaxExpansionHeight);
187            }
188        } else {
189            if (!mStackScrollerOverscrolling) {
190                setQsExpansion(mQsMinExpansionHeight);
191            }
192            positionClockAndNotifications();
193            mNotificationStackScroller.setStackHeight(getExpandedHeight());
194        }
195    }
196
197    /**
198     * Positions the clock and notifications dynamically depending on how many notifications are
199     * showing.
200     */
201    private void positionClockAndNotifications() {
202        boolean animateClock = mNotificationStackScroller.isAddOrRemoveAnimationPending();
203        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
204            int bottom = mStackScrollerOverscrolling
205                    ? mHeader.getCollapsedHeight()
206                    : mHeader.getBottom();
207            mStackScrollerIntrinsicPadding = bottom + mQsPeekHeight
208                    + mNotificationTopPadding;
209            mTopPaddingAdjustment = 0;
210        } else {
211            mClockPositionAlgorithm.setup(
212                    mStatusBar.getMaxKeyguardNotifications(),
213                    getMaxPanelHeight(),
214                    getExpandedHeight(),
215                    mNotificationStackScroller.getNotGoneChildCount(),
216                    getHeight(),
217                    mKeyguardStatusView.getHeight());
218            mClockPositionAlgorithm.run(mClockPositionResult);
219            if (animateClock || mClockAnimator != null) {
220                startClockAnimation(mClockPositionResult.clockY);
221            } else {
222                mKeyguardStatusView.setY(mClockPositionResult.clockY);
223            }
224            applyClockAlpha(mClockPositionResult.clockAlpha);
225            mStackScrollerIntrinsicPadding = mClockPositionResult.stackScrollerPadding;
226            mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
227        }
228        mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding,
229                mAnimateNextTopPaddingChange || animateClock);
230        mAnimateNextTopPaddingChange = false;
231    }
232
233    private void startClockAnimation(int y) {
234        if (mClockAnimationTarget == y) {
235            return;
236        }
237        mClockAnimationTarget = y;
238        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
239            @Override
240            public boolean onPreDraw() {
241                getViewTreeObserver().removeOnPreDrawListener(this);
242                if (mClockAnimator != null) {
243                    mClockAnimator.removeAllListeners();
244                    mClockAnimator.cancel();
245                }
246                mClockAnimator = ObjectAnimator
247                        .ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget);
248                mClockAnimator.setInterpolator(mFastOutSlowInInterpolator);
249                mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
250                mClockAnimator.addListener(new AnimatorListenerAdapter() {
251                    @Override
252                    public void onAnimationEnd(Animator animation) {
253                        mClockAnimator = null;
254                        mClockAnimationTarget = -1;
255                    }
256                });
257                mClockAnimator.start();
258                return true;
259            }
260        });
261    }
262
263    private void applyClockAlpha(float alpha) {
264        if (alpha != 1.0f) {
265            mKeyguardStatusView.setLayerType(LAYER_TYPE_HARDWARE, null);
266        } else {
267            mKeyguardStatusView.setLayerType(LAYER_TYPE_NONE, null);
268        }
269        mKeyguardStatusView.setAlpha(alpha);
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        mPageSwiper.reset();
293        closeQs();
294    }
295
296    public void closeQs() {
297        cancelAnimation();
298        setQsExpansion(mQsMinExpansionHeight);
299    }
300
301    public void openQs() {
302        cancelAnimation();
303        if (mQsExpansionEnabled) {
304            setQsExpansion(mQsMaxExpansionHeight);
305        }
306    }
307
308    @Override
309    public void fling(float vel, boolean always) {
310        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
311        if (gr != null) {
312            gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
313        }
314        super.fling(vel, always);
315    }
316
317    @Override
318    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
319        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
320            event.getText()
321                    .add(getContext().getString(R.string.accessibility_desc_notification_shade));
322            return true;
323        }
324
325        return super.dispatchPopulateAccessibilityEvent(event);
326    }
327
328    @Override
329    public boolean onInterceptTouchEvent(MotionEvent event) {
330        if (mBlockTouches) {
331            return false;
332        }
333        int pointerIndex = event.findPointerIndex(mTrackingPointer);
334        if (pointerIndex < 0) {
335            pointerIndex = 0;
336            mTrackingPointer = event.getPointerId(pointerIndex);
337        }
338        final float x = event.getX(pointerIndex);
339        final float y = event.getY(pointerIndex);
340
341        switch (event.getActionMasked()) {
342            case MotionEvent.ACTION_DOWN:
343                mIntercepting = true;
344                mInitialTouchY = y;
345                mInitialTouchX = x;
346                initVelocityTracker();
347                trackMovement(event);
348                if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
349                    getParent().requestDisallowInterceptTouchEvent(true);
350                }
351                break;
352            case MotionEvent.ACTION_POINTER_UP:
353                final int upPointer = event.getPointerId(event.getActionIndex());
354                if (mTrackingPointer == upPointer) {
355                    // gesture is ongoing, find a new pointer to track
356                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
357                    mTrackingPointer = event.getPointerId(newIndex);
358                    mInitialTouchX = event.getX(newIndex);
359                    mInitialTouchY = event.getY(newIndex);
360                }
361                break;
362
363            case MotionEvent.ACTION_MOVE:
364                final float h = y - mInitialTouchY;
365                trackMovement(event);
366                if (mQsTracking) {
367
368                    // Already tracking because onOverscrolled was called. We need to update here
369                    // so we don't stop for a frame until the next touch event gets handled in
370                    // onTouchEvent.
371                    setQsExpansion(h + mInitialHeightOnTouch);
372                    trackMovement(event);
373                    mIntercepting = false;
374                    return true;
375                }
376                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
377                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
378                    onQsExpansionStarted();
379                    mInitialHeightOnTouch = mQsExpansionHeight;
380                    mInitialTouchY = y;
381                    mInitialTouchX = x;
382                    mQsTracking = true;
383                    mIntercepting = false;
384                    return true;
385                }
386                break;
387
388            case MotionEvent.ACTION_CANCEL:
389            case MotionEvent.ACTION_UP:
390                trackMovement(event);
391                if (mQsTracking) {
392                    flingQsWithCurrentVelocity();
393                    mQsTracking = false;
394                }
395                mIntercepting = false;
396                break;
397        }
398        return !mQsExpanded && super.onInterceptTouchEvent(event);
399    }
400
401    @Override
402    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
403
404        // Block request when interacting with the scroll view so we can still intercept the
405        // scrolling when QS is expanded.
406        if (mScrollView.isDispatchingTouchEvent()) {
407            return;
408        }
409        super.requestDisallowInterceptTouchEvent(disallowIntercept);
410    }
411
412    private void flingQsWithCurrentVelocity() {
413        float vel = getCurrentVelocity();
414
415        // TODO: Better logic whether we should expand or not.
416        flingSettings(vel, vel > 0);
417    }
418
419    @Override
420    public boolean onTouchEvent(MotionEvent event) {
421        if (mBlockTouches) {
422            return false;
423        }
424        // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference
425        // implementation.
426        if ((!mIsExpanding || mHintAnimationRunning)
427                && !mQsExpanded
428                && mStatusBar.getBarState() != StatusBarState.SHADE) {
429            mPageSwiper.onTouchEvent(event);
430            if (mPageSwiper.isSwipingInProgress()) {
431                return true;
432            }
433        }
434        if (mQsTracking || mQsExpanded) {
435            return onQsTouch(event);
436        }
437
438        super.onTouchEvent(event);
439        return true;
440    }
441
442    @Override
443    protected boolean hasConflictingGestures() {
444        return mStatusBar.getBarState() != StatusBarState.SHADE;
445    }
446
447    private boolean onQsTouch(MotionEvent event) {
448        int pointerIndex = event.findPointerIndex(mTrackingPointer);
449        if (pointerIndex < 0) {
450            pointerIndex = 0;
451            mTrackingPointer = event.getPointerId(pointerIndex);
452        }
453        final float y = event.getY(pointerIndex);
454        final float x = event.getX(pointerIndex);
455
456        switch (event.getActionMasked()) {
457            case MotionEvent.ACTION_DOWN:
458                mQsTracking = true;
459                mInitialTouchY = y;
460                mInitialTouchX = x;
461                onQsExpansionStarted();
462                mInitialHeightOnTouch = mQsExpansionHeight;
463                initVelocityTracker();
464                trackMovement(event);
465                break;
466
467            case MotionEvent.ACTION_POINTER_UP:
468                final int upPointer = event.getPointerId(event.getActionIndex());
469                if (mTrackingPointer == upPointer) {
470                    // gesture is ongoing, find a new pointer to track
471                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
472                    final float newY = event.getY(newIndex);
473                    final float newX = event.getX(newIndex);
474                    mTrackingPointer = event.getPointerId(newIndex);
475                    mInitialHeightOnTouch = mQsExpansionHeight;
476                    mInitialTouchY = newY;
477                    mInitialTouchX = newX;
478                }
479                break;
480
481            case MotionEvent.ACTION_MOVE:
482                final float h = y - mInitialTouchY;
483                setQsExpansion(h + mInitialHeightOnTouch);
484                trackMovement(event);
485                break;
486
487            case MotionEvent.ACTION_UP:
488            case MotionEvent.ACTION_CANCEL:
489                mQsTracking = false;
490                mTrackingPointer = -1;
491                trackMovement(event);
492                flingQsWithCurrentVelocity();
493                if (mVelocityTracker != null) {
494                    mVelocityTracker.recycle();
495                    mVelocityTracker = null;
496                }
497                break;
498        }
499        return true;
500    }
501
502    @Override
503    public void onOverscrolled(int amount) {
504        if (mIntercepting) {
505            onQsExpansionStarted(amount);
506            mInitialHeightOnTouch = mQsExpansionHeight;
507            mInitialTouchY = mLastTouchY;
508            mInitialTouchX = mLastTouchX;
509            mQsTracking = true;
510        }
511    }
512
513
514    @Override
515    public void onOverscrollTopChanged(float amount) {
516        cancelAnimation();
517        float rounded = amount >= 1f ? amount : 0f;
518        mStackScrollerOverscrolling = rounded != 0f;
519        setQsExpansion(mQsMinExpansionHeight + rounded);
520        updateQsState();
521    }
522
523    private void onQsExpansionStarted() {
524        onQsExpansionStarted(0);
525    }
526
527    private void onQsExpansionStarted(int overscrollAmount) {
528        cancelAnimation();
529
530        // Reset scroll position and apply that position to the expanded height.
531        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
532        mScrollView.scrollTo(0, 0);
533        setQsExpansion(height);
534    }
535
536    private void setQsExpanded(boolean expanded) {
537        boolean changed = mQsExpanded != expanded;
538        if (changed) {
539            mQsExpanded = expanded;
540            updateQsState();
541        }
542    }
543
544    public void setKeyguardShowing(boolean keyguardShowing) {
545        mKeyguardShowing = keyguardShowing;
546        updateQsState();
547    }
548
549    private void updateQsState() {
550        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling;
551        mHeader.setExpanded(expandVisually, mStackScrollerOverscrolling);
552        mNotificationStackScroller.setEnabled(!mQsExpanded);
553        mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
554        mQsContainer.setVisibility(mKeyguardShowing && !mQsExpanded ? View.INVISIBLE : View.VISIBLE);
555        mScrollView.setTouchEnabled(mQsExpanded);
556    }
557
558    private void setQsExpansion(float height) {
559        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
560        mQsFullyExpanded = height == mQsMaxExpansionHeight;
561        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
562            setQsExpanded(true);
563        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
564            setQsExpanded(false);
565        }
566        mQsExpansionHeight = height;
567        mHeader.setExpansion(height - mQsPeekHeight);
568        setQsTranslation(height);
569        if (!mStackScrollerOverscrolling) {
570            setQsStackScrollerPadding(height);
571        }
572        mStatusBar.userActivity();
573    }
574
575    private void setQsTranslation(float height) {
576        mQsContainer.setY(height - mQsContainer.getHeight());
577    }
578
579    private void setQsStackScrollerPadding(float height) {
580        float start = height - mScrollView.getScrollY() + mNotificationTopPadding;
581        float stackHeight = mNotificationStackScroller.getHeight() - start;
582        if (stackHeight <= mMinStackHeight) {
583            float overflow = mMinStackHeight - stackHeight;
584            stackHeight = mMinStackHeight;
585            start = mNotificationStackScroller.getHeight() - stackHeight;
586            mNotificationStackScroller.setTranslationY(overflow);
587            mNotificationTranslation = overflow + mScrollView.getScrollY();
588        } else {
589            mNotificationStackScroller.setTranslationY(0);
590            mNotificationTranslation = mScrollView.getScrollY();
591        }
592        mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false);
593    }
594
595    private int clampQsStackScrollerPadding(int desiredPadding) {
596        return Math.max(desiredPadding, mStackScrollerIntrinsicPadding);
597    }
598
599    private void trackMovement(MotionEvent event) {
600        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
601        mLastTouchX = event.getX();
602        mLastTouchY = event.getY();
603    }
604
605    private void initVelocityTracker() {
606        if (mVelocityTracker != null) {
607            mVelocityTracker.recycle();
608        }
609        mVelocityTracker = VelocityTracker.obtain();
610    }
611
612    private float getCurrentVelocity() {
613        if (mVelocityTracker == null) {
614            return 0;
615        }
616        mVelocityTracker.computeCurrentVelocity(1000);
617        return mVelocityTracker.getYVelocity();
618    }
619
620    private void cancelAnimation() {
621        if (mQsExpansionAnimator != null) {
622            mQsExpansionAnimator.cancel();
623        }
624    }
625    private void flingSettings(float vel, boolean expand) {
626        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
627        if (target == mQsExpansionHeight) {
628            return;
629        }
630        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
631        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
632        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
633            @Override
634            public void onAnimationUpdate(ValueAnimator animation) {
635                setQsExpansion((Float) animation.getAnimatedValue());
636            }
637        });
638        animator.addListener(new AnimatorListenerAdapter() {
639            @Override
640            public void onAnimationEnd(Animator animation) {
641                mQsExpansionAnimator = null;
642            }
643        });
644        animator.start();
645        mQsExpansionAnimator = animator;
646    }
647
648    /**
649     * @return Whether we should intercept a gesture to open Quick Settings.
650     */
651    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
652        if (!mQsExpansionEnabled) {
653            return false;
654        }
655        boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight()
656                && y >= mHeader.getTop() && y <= mHeader.getBottom();
657        if (mQsExpanded) {
658            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
659        } else {
660            return onHeader;
661        }
662    }
663
664    @Override
665    public void setVisibility(int visibility) {
666        int oldVisibility = getVisibility();
667        super.setVisibility(visibility);
668        if (visibility != oldVisibility) {
669            reparentStatusIcons(visibility == VISIBLE);
670        }
671    }
672
673    /**
674     * When the notification panel gets expanded, we need to move the status icons in the header
675     * card.
676     */
677    private void reparentStatusIcons(boolean toHeader) {
678        if (mStatusBar == null) {
679            return;
680        }
681        LinearLayout systemIcons = mStatusBar.getSystemIcons();
682        if (systemIcons.getParent() != null) {
683            ((ViewGroup) systemIcons.getParent()).removeView(systemIcons);
684        }
685        if (toHeader) {
686            mHeader.attachSystemIcons(systemIcons);
687        } else {
688            mHeader.onSystemIconsDetached();
689            mStatusBar.reattachSystemIcons();
690        }
691    }
692
693    @Override
694    protected boolean isScrolledToBottom() {
695        if (!isInSettings()) {
696            return mNotificationStackScroller.isScrolledToBottom();
697        }
698        return super.isScrolledToBottom();
699    }
700
701    @Override
702    protected int getMaxPanelHeight() {
703        // TODO: Figure out transition for collapsing when QS is open, adjust height here.
704        int maxPanelHeight = super.getMaxPanelHeight();
705        int emptyBottomMargin = mStackScrollerContainer.getHeight()
706                - mNotificationStackScroller.getHeight()
707                + mNotificationStackScroller.getEmptyBottomMargin();
708        int maxHeight = maxPanelHeight - emptyBottomMargin - mTopPaddingAdjustment;
709        maxHeight = Math.max(maxHeight, mStatusBarMinHeight);
710        return maxHeight;
711    }
712
713    private boolean isInSettings() {
714        return mQsExpanded;
715    }
716
717    @Override
718    protected void onHeightUpdated(float expandedHeight) {
719        if (!mQsExpanded) {
720            positionClockAndNotifications();
721        }
722        mNotificationStackScroller.setStackHeight(expandedHeight);
723        updateKeyguardHeaderVisibility();
724    }
725
726    /**
727     * Hides the header when notifications are colliding with it.
728     */
729    private void updateKeyguardHeaderVisibility() {
730        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
731                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
732            boolean hidden;
733            if (mStatusBar.getBarState() == StatusBarState.KEYGUARD) {
734
735                // When on Keyguard, we hide the header as soon as the top card of the notification
736                // stack scroller is close enough (collision distance) to the bottom of the header.
737                hidden = mNotificationStackScroller.getNotificationsTopY()
738                        <= mHeader.getBottom() + mNotificationsHeaderCollideDistance;
739            } else {
740
741                // In SHADE_LOCKED, the top card is already really close to the header. Hide it as
742                // soon as we start translating the stack.
743                hidden = mNotificationStackScroller.getTranslationY() < 0;
744            }
745
746            if (hidden && !mHeaderHidden) {
747                mHeader.animate()
748                        .alpha(0f)
749                        .withLayer()
750                        .translationY(-mHeader.getHeight()/2)
751                        .setInterpolator(mFastOutLinearInterpolator)
752                        .setDuration(200);
753            } else if (!hidden && mHeaderHidden) {
754                mHeader.animate()
755                        .alpha(1f)
756                        .withLayer()
757                        .translationY(0)
758                        .setInterpolator(mLinearOutSlowInInterpolator)
759                        .setDuration(200);
760            }
761            mHeaderHidden = hidden;
762        } else {
763            mHeader.animate().cancel();
764            mHeader.setAlpha(1f);
765            mHeader.setTranslationY(0f);
766            if (mHeader.getLayerType() != LAYER_TYPE_NONE) {
767                mHeader.setLayerType(LAYER_TYPE_NONE, null);
768            }
769            mHeaderHidden = false;
770        }
771
772    }
773
774    @Override
775    protected void onExpandingStarted() {
776        super.onExpandingStarted();
777        mNotificationStackScroller.onExpansionStarted();
778        mIsExpanding = true;
779    }
780
781    @Override
782    protected void onExpandingFinished() {
783        super.onExpandingFinished();
784        mNotificationStackScroller.onExpansionStopped();
785        mIsExpanding = false;
786    }
787
788    @Override
789    protected void onOverExpansionChanged(float overExpansion) {
790        float currentOverScroll = mNotificationStackScroller.getCurrentOverScrolledPixels(true);
791        float expansionChange = overExpansion - mOverExpansion;
792        expansionChange *= EXPANSION_RUBBER_BAND_EXTRA_FACTOR;
793        mNotificationStackScroller.setOverScrolledPixels(currentOverScroll + expansionChange,
794                true /* onTop */,
795                false /* animate */);
796        super.onOverExpansionChanged(overExpansion);
797    }
798
799    @Override
800    protected void onTrackingStarted() {
801        super.onTrackingStarted();
802        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
803                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
804            mPageSwiper.animateHideLeftRightIcon();
805        }
806    }
807
808    @Override
809    protected void onTrackingStopped(boolean expand) {
810        super.onTrackingStopped(expand);
811        mOverExpansion = 0.0f;
812        mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */, true /* animate */);
813        if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
814                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
815            mPageSwiper.showAllIcons(true);
816        }
817    }
818
819    @Override
820    public void onHeightChanged(ExpandableView view) {
821        requestPanelHeightUpdate();
822    }
823
824    @Override
825    public void onScrollChanged() {
826        if (mQsExpanded) {
827            mNotificationStackScroller.setTranslationY(
828                    mNotificationTranslation - mScrollView.getScrollY());
829        }
830    }
831
832    @Override
833    protected void onConfigurationChanged(Configuration newConfig) {
834        super.onConfigurationChanged(newConfig);
835        mPageSwiper.onConfigurationChanged();
836    }
837
838    @Override
839    public void onClick(View v) {
840        if (v == mHeader.getBackgroundView()) {
841            onQsExpansionStarted();
842            if (mQsExpanded) {
843                flingSettings(0 /* vel */, false /* expand */);
844            } else if (mQsExpansionEnabled) {
845                flingSettings(0 /* vel */, true /* expand */);
846            }
847        }
848    }
849
850    @Override
851    public void onAnimationToSideStarted(boolean rightPage) {
852        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
853        if (start) {
854            mKeyguardBottomArea.launchPhone();
855        } else {
856            mKeyguardBottomArea.launchCamera();
857        }
858        mBlockTouches = true;
859    }
860
861    @Override
862    protected void onEdgeClicked(boolean right) {
863        if ((right && getRightIcon().getVisibility() != View.VISIBLE)
864                || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) {
865            return;
866        }
867        mHintAnimationRunning = true;
868        mPageSwiper.startHintAnimation(right, new Runnable() {
869            @Override
870            public void run() {
871                mHintAnimationRunning = false;
872                mStatusBar.onHintFinished();
873            }
874        });
875        startHighlightIconAnimation(right ? getRightIcon() : getLeftIcon());
876        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right;
877        if (start) {
878            mStatusBar.onPhoneHintStarted();
879        } else {
880            mStatusBar.onCameraHintStarted();
881        }
882    }
883
884    @Override
885    protected void startUnlockHintAnimation() {
886        super.startUnlockHintAnimation();
887        startHighlightIconAnimation(getCenterIcon());
888    }
889
890    /**
891     * Starts the highlight (making it fully opaque) animation on an icon.
892     */
893    private void startHighlightIconAnimation(final View icon) {
894        icon.animate()
895                .alpha(1.0f)
896                .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION)
897                .setInterpolator(mFastOutSlowInInterpolator)
898                .withEndAction(new Runnable() {
899                    @Override
900                    public void run() {
901                        icon.animate().alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT)
902                                .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION)
903                                .setInterpolator(mFastOutSlowInInterpolator);
904                    }
905                });
906    }
907
908    @Override
909    public float getPageWidth() {
910        return getWidth();
911    }
912
913    @Override
914    public ArrayList<View> getTranslationViews() {
915        return mSwipeTranslationViews;
916    }
917
918    @Override
919    public View getLeftIcon() {
920        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
921                ? mKeyguardBottomArea.getCameraImageView()
922                : mKeyguardBottomArea.getPhoneImageView();
923    }
924
925    @Override
926    public View getCenterIcon() {
927        return mKeyguardBottomArea.getLockIcon();
928    }
929
930    @Override
931    public View getRightIcon() {
932        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
933                ? mKeyguardBottomArea.getPhoneImageView()
934                : mKeyguardBottomArea.getCameraImageView();
935    }
936}
937