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