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