NotificationPanelView.java revision 8dd95e03f8555c278217bac7e4faef865b1850a7
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 mMinStackHeight;
87    private int mQsPeekHeight;
88    private float mNotificationTranslation;
89    private int mStackScrollerIntrinsicPadding;
90    private boolean mStackScrollerOverscrolling;
91    private boolean mQsExpansionEnabled = true;
92    private ValueAnimator mQsExpansionAnimator;
93    private FlingAnimationUtils mFlingAnimationUtils;
94    private int mStatusBarMinHeight;
95    private boolean mHeaderHidden;
96    private boolean mUnlockIconActive;
97    private int mNotificationsHeaderCollideDistance;
98    private int mUnlockMoveDistance;
99
100    private Interpolator mFastOutSlowInInterpolator;
101    private Interpolator mFastOutLinearInterpolator;
102    private Interpolator mLinearOutSlowInInterpolator;
103    private ObjectAnimator mClockAnimator;
104    private int mClockAnimationTarget = -1;
105    private int mTopPaddingAdjustment;
106    private KeyguardClockPositionAlgorithm mClockPositionAlgorithm =
107            new KeyguardClockPositionAlgorithm();
108    private KeyguardClockPositionAlgorithm.Result mClockPositionResult =
109            new KeyguardClockPositionAlgorithm.Result();
110    private boolean mIsSwipedHorizontally;
111    private boolean mIsExpanding;
112    private KeyguardBottomAreaView mKeyguardBottomArea;
113    private boolean mBlockTouches;
114    private ArrayList<View> mSwipeTranslationViews = new ArrayList<>();
115
116    public NotificationPanelView(Context context, AttributeSet attrs) {
117        super(context, attrs);
118    }
119
120    public void setStatusBar(PhoneStatusBar bar) {
121        if (mStatusBar != null) {
122            mStatusBar.setOnFlipRunnable(null);
123        }
124        mStatusBar = bar;
125        if (bar != null) {
126            mStatusBar.setOnFlipRunnable(new Runnable() {
127                @Override
128                public void run() {
129                    requestPanelHeightUpdate();
130                }
131            });
132        }
133    }
134
135    @Override
136    protected void onFinishInflate() {
137        super.onFinishInflate();
138        mHeader = (StatusBarHeaderView) findViewById(R.id.header);
139        mHeader.getBackgroundView().setOnClickListener(this);
140        mHeader.setOverlayParent(this);
141        mKeyguardStatusView = findViewById(R.id.keyguard_status_view);
142        mStackScrollerContainer = findViewById(R.id.notification_container_parent);
143        mQsContainer = findViewById(R.id.quick_settings_container);
144        mQsPanel = findViewById(R.id.quick_settings_panel);
145        mScrollView = (ObservableScrollView) findViewById(R.id.scroll_view);
146        mScrollView.setListener(this);
147        mNotificationStackScroller = (NotificationStackScrollLayout)
148                findViewById(R.id.notification_stack_scroller);
149        mNotificationStackScroller.setOnHeightChangedListener(this);
150        mNotificationStackScroller.setOverscrollTopChangedListener(this);
151        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
152                android.R.interpolator.fast_out_slow_in);
153        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(),
154                android.R.interpolator.linear_out_slow_in);
155        mFastOutLinearInterpolator = AnimationUtils.loadInterpolator(getContext(),
156                android.R.interpolator.fast_out_linear_in);
157        mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area);
158        mSwipeTranslationViews.add(mNotificationStackScroller);
159        mSwipeTranslationViews.add(mKeyguardStatusView);
160        mPageSwiper = new KeyguardPageSwipeHelper(this, getContext());
161    }
162
163    @Override
164    protected void loadDimens() {
165        super.loadDimens();
166        mNotificationTopPadding = getResources().getDimensionPixelSize(
167                R.dimen.notifications_top_padding);
168        mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height);
169        mFlingAnimationUtils = new FlingAnimationUtils(getContext(), 0.4f);
170        mStatusBarMinHeight = getResources().getDimensionPixelSize(
171                com.android.internal.R.dimen.status_bar_height);
172        mQsPeekHeight = getResources().getDimensionPixelSize(R.dimen.qs_peek_height);
173        mNotificationsHeaderCollideDistance =
174                getResources().getDimensionPixelSize(R.dimen.header_notifications_collide_distance);
175        mUnlockMoveDistance = getResources().getDimensionPixelOffset(R.dimen.unlock_move_distance);
176        mClockPositionAlgorithm.loadDimens(getResources());
177    }
178
179    @Override
180    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
181        super.onLayout(changed, left, top, right, bottom);
182
183        // Calculate quick setting heights.
184        mQsMinExpansionHeight = mHeader.getCollapsedHeight() + mQsPeekHeight;
185        mQsMaxExpansionHeight = mHeader.getExpandedHeight() + mQsContainer.getHeight();
186        if (mQsExpanded) {
187            if (mQsFullyExpanded) {
188                setQsStackScrollerPadding(mQsMaxExpansionHeight);
189            }
190        } else {
191            if (!mStackScrollerOverscrolling) {
192                setQsExpansion(mQsMinExpansionHeight);
193            }
194            positionClockAndNotifications();
195            mNotificationStackScroller.setStackHeight(getExpandedHeight());
196        }
197    }
198
199    /**
200     * Positions the clock and notifications dynamically depending on how many notifications are
201     * showing.
202     */
203    private void positionClockAndNotifications() {
204        boolean animateClock = mNotificationStackScroller.isAddOrRemoveAnimationPending();
205        if (mStatusBar.getBarState() != StatusBarState.KEYGUARD) {
206            int bottom = mStackScrollerOverscrolling
207                    ? mHeader.getCollapsedHeight()
208                    : mHeader.getBottom();
209            mStackScrollerIntrinsicPadding = 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            mStackScrollerIntrinsicPadding = mClockPositionResult.stackScrollerPadding;
228            mTopPaddingAdjustment = mClockPositionResult.stackScrollerPaddingAdjustment;
229        }
230        mNotificationStackScroller.setTopPadding(mStackScrollerIntrinsicPadding,
231                mAnimateNextTopPaddingChange || animateClock);
232        mAnimateNextTopPaddingChange = false;
233    }
234
235    private void startClockAnimation(int y) {
236        if (mClockAnimationTarget == y) {
237            return;
238        }
239        mClockAnimationTarget = y;
240        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
241            @Override
242            public boolean onPreDraw() {
243                getViewTreeObserver().removeOnPreDrawListener(this);
244                if (mClockAnimator != null) {
245                    mClockAnimator.removeAllListeners();
246                    mClockAnimator.cancel();
247                }
248                mClockAnimator =
249                        ObjectAnimator.ofFloat(mKeyguardStatusView, View.Y, mClockAnimationTarget);
250                mClockAnimator.setInterpolator(mFastOutSlowInInterpolator);
251                mClockAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
252                mClockAnimator.addListener(new AnimatorListenerAdapter() {
253                    @Override
254                    public void onAnimationEnd(Animator animation) {
255                        mClockAnimator = null;
256                        mClockAnimationTarget = -1;
257                    }
258                });
259                mClockAnimator.start();
260                return true;
261            }
262        });
263    }
264
265    private void applyClockAlpha(float alpha) {
266        if (alpha != 1.0f) {
267            mKeyguardStatusView.setLayerType(LAYER_TYPE_HARDWARE, null);
268        } else {
269            mKeyguardStatusView.setLayerType(LAYER_TYPE_NONE, null);
270        }
271        mKeyguardStatusView.setAlpha(alpha);
272    }
273
274    public void animateToFullShade() {
275        mAnimateNextTopPaddingChange = true;
276        mNotificationStackScroller.goToFullShade();
277        requestLayout();
278    }
279
280    /**
281     * @return Whether Quick Settings are currently expanded.
282     */
283    public boolean isQsExpanded() {
284        return mQsExpanded;
285    }
286
287    public void setQsExpansionEnabled(boolean qsExpansionEnabled) {
288        mQsExpansionEnabled = qsExpansionEnabled;
289    }
290
291    @Override
292    public void resetViews() {
293        mBlockTouches = false;
294        mUnlockIconActive = false;
295        mPageSwiper.reset();
296        closeQs();
297    }
298
299    public void closeQs() {
300        cancelAnimation();
301        setQsExpansion(mQsMinExpansionHeight);
302    }
303
304    public void openQs() {
305        cancelAnimation();
306        if (mQsExpansionEnabled) {
307            setQsExpansion(mQsMaxExpansionHeight);
308        }
309    }
310
311    @Override
312    public void fling(float vel, boolean always) {
313        GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder();
314        if (gr != null) {
315            gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel);
316        }
317        super.fling(vel, always);
318    }
319
320    @Override
321    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
322        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
323            event.getText()
324                    .add(getContext().getString(R.string.accessibility_desc_notification_shade));
325            return true;
326        }
327
328        return super.dispatchPopulateAccessibilityEvent(event);
329    }
330
331    @Override
332    public boolean onInterceptTouchEvent(MotionEvent event) {
333        if (mBlockTouches) {
334            return false;
335        }
336        int pointerIndex = event.findPointerIndex(mTrackingPointer);
337        if (pointerIndex < 0) {
338            pointerIndex = 0;
339            mTrackingPointer = event.getPointerId(pointerIndex);
340        }
341        final float x = event.getX(pointerIndex);
342        final float y = event.getY(pointerIndex);
343
344        switch (event.getActionMasked()) {
345            case MotionEvent.ACTION_DOWN:
346                mIntercepting = true;
347                mInitialTouchY = y;
348                mInitialTouchX = x;
349                initVelocityTracker();
350                trackMovement(event);
351                if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) {
352                    getParent().requestDisallowInterceptTouchEvent(true);
353                }
354                break;
355            case MotionEvent.ACTION_POINTER_UP:
356                final int upPointer = event.getPointerId(event.getActionIndex());
357                if (mTrackingPointer == upPointer) {
358                    // gesture is ongoing, find a new pointer to track
359                    final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
360                    mTrackingPointer = event.getPointerId(newIndex);
361                    mInitialTouchX = event.getX(newIndex);
362                    mInitialTouchY = event.getY(newIndex);
363                }
364                break;
365
366            case MotionEvent.ACTION_MOVE:
367                final float h = y - mInitialTouchY;
368                trackMovement(event);
369                if (mQsTracking) {
370
371                    // Already tracking because onOverscrolled was called. We need to update here
372                    // so we don't stop for a frame until the next touch event gets handled in
373                    // onTouchEvent.
374                    setQsExpansion(h + mInitialHeightOnTouch);
375                    trackMovement(event);
376                    mIntercepting = false;
377                    return true;
378                }
379                if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX)
380                        && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) {
381                    onQsExpansionStarted();
382                    mInitialHeightOnTouch = mQsExpansionHeight;
383                    mInitialTouchY = y;
384                    mInitialTouchX = x;
385                    mQsTracking = true;
386                    mIntercepting = false;
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    private void onQsExpansionStarted() {
527        onQsExpansionStarted(0);
528    }
529
530    private void onQsExpansionStarted(int overscrollAmount) {
531        cancelAnimation();
532
533        // Reset scroll position and apply that position to the expanded height.
534        float height = mQsExpansionHeight - mScrollView.getScrollY() - overscrollAmount;
535        mScrollView.scrollTo(0, 0);
536        setQsExpansion(height);
537    }
538
539    private void setQsExpanded(boolean expanded) {
540        boolean changed = mQsExpanded != expanded;
541        if (changed) {
542            mQsExpanded = expanded;
543            updateQsState();
544        }
545    }
546
547    public void setKeyguardShowing(boolean keyguardShowing) {
548        mKeyguardShowing = keyguardShowing;
549        updateQsState();
550    }
551
552    private void updateQsState() {
553        boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling;
554        mHeader.setExpanded(expandVisually, mStackScrollerOverscrolling);
555        mNotificationStackScroller.setEnabled(!mQsExpanded);
556        mQsPanel.setVisibility(expandVisually ? View.VISIBLE : View.INVISIBLE);
557        mQsContainer.setVisibility(mKeyguardShowing && !mQsExpanded
558                ? View.INVISIBLE
559                : View.VISIBLE);
560        mScrollView.setTouchEnabled(mQsExpanded);
561    }
562
563    private void setQsExpansion(float height) {
564        height = Math.min(Math.max(height, mQsMinExpansionHeight), mQsMaxExpansionHeight);
565        mQsFullyExpanded = height == mQsMaxExpansionHeight;
566        if (height > mQsMinExpansionHeight && !mQsExpanded && !mStackScrollerOverscrolling) {
567            setQsExpanded(true);
568        } else if (height <= mQsMinExpansionHeight && mQsExpanded) {
569            setQsExpanded(false);
570        }
571        mQsExpansionHeight = height;
572        mHeader.setExpansion(height - mQsPeekHeight);
573        setQsTranslation(height);
574        if (!mStackScrollerOverscrolling) {
575            setQsStackScrollerPadding(height);
576        }
577        mStatusBar.userActivity();
578    }
579
580    private void setQsTranslation(float height) {
581        mQsContainer.setY(height - mQsContainer.getHeight());
582    }
583
584    private void setQsStackScrollerPadding(float height) {
585        float start = height - mScrollView.getScrollY() + mNotificationTopPadding;
586        float stackHeight = mNotificationStackScroller.getHeight() - start;
587        if (stackHeight <= mMinStackHeight) {
588            float overflow = mMinStackHeight - stackHeight;
589            stackHeight = mMinStackHeight;
590            start = mNotificationStackScroller.getHeight() - stackHeight;
591            mNotificationStackScroller.setTranslationY(overflow);
592            mNotificationTranslation = overflow + mScrollView.getScrollY();
593        } else {
594            mNotificationStackScroller.setTranslationY(0);
595            mNotificationTranslation = mScrollView.getScrollY();
596        }
597        mNotificationStackScroller.setTopPadding(clampQsStackScrollerPadding((int) start), false);
598    }
599
600    private int clampQsStackScrollerPadding(int desiredPadding) {
601        return Math.max(desiredPadding, mStackScrollerIntrinsicPadding);
602    }
603
604    private void trackMovement(MotionEvent event) {
605        if (mVelocityTracker != null) mVelocityTracker.addMovement(event);
606        mLastTouchX = event.getX();
607        mLastTouchY = event.getY();
608    }
609
610    private void initVelocityTracker() {
611        if (mVelocityTracker != null) {
612            mVelocityTracker.recycle();
613        }
614        mVelocityTracker = VelocityTracker.obtain();
615    }
616
617    private float getCurrentVelocity() {
618        if (mVelocityTracker == null) {
619            return 0;
620        }
621        mVelocityTracker.computeCurrentVelocity(1000);
622        return mVelocityTracker.getYVelocity();
623    }
624
625    private void cancelAnimation() {
626        if (mQsExpansionAnimator != null) {
627            mQsExpansionAnimator.cancel();
628        }
629    }
630    private void flingSettings(float vel, boolean expand) {
631        float target = expand ? mQsMaxExpansionHeight : mQsMinExpansionHeight;
632        if (target == mQsExpansionHeight) {
633            return;
634        }
635        ValueAnimator animator = ValueAnimator.ofFloat(mQsExpansionHeight, target);
636        mFlingAnimationUtils.apply(animator, mQsExpansionHeight, target, vel);
637        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
638            @Override
639            public void onAnimationUpdate(ValueAnimator animation) {
640                setQsExpansion((Float) animation.getAnimatedValue());
641            }
642        });
643        animator.addListener(new AnimatorListenerAdapter() {
644            @Override
645            public void onAnimationEnd(Animator animation) {
646                mQsExpansionAnimator = null;
647            }
648        });
649        animator.start();
650        mQsExpansionAnimator = animator;
651    }
652
653    /**
654     * @return Whether we should intercept a gesture to open Quick Settings.
655     */
656    private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) {
657        if (!mQsExpansionEnabled) {
658            return false;
659        }
660        boolean onHeader = x >= mHeader.getLeft() && x <= mHeader.getRight()
661                && y >= mHeader.getTop() && y <= mHeader.getBottom();
662        if (mQsExpanded) {
663            return onHeader || (mScrollView.isScrolledToBottom() && yDiff < 0);
664        } else {
665            return onHeader;
666        }
667    }
668
669    @Override
670    public void setVisibility(int visibility) {
671        int oldVisibility = getVisibility();
672        super.setVisibility(visibility);
673        if (visibility != oldVisibility) {
674            reparentStatusIcons(visibility == VISIBLE);
675        }
676    }
677
678    /**
679     * When the notification panel gets expanded, we need to move the status icons in the header
680     * card.
681     */
682    private void reparentStatusIcons(boolean toHeader) {
683        if (mStatusBar == null) {
684            return;
685        }
686        LinearLayout systemIcons = mStatusBar.getSystemIcons();
687        if (systemIcons.getParent() != null) {
688            ((ViewGroup) systemIcons.getParent()).removeView(systemIcons);
689        }
690        if (toHeader) {
691            mHeader.attachSystemIcons(systemIcons);
692        } else {
693            mHeader.onSystemIconsDetached();
694            mStatusBar.reattachSystemIcons();
695        }
696    }
697
698    @Override
699    protected boolean isScrolledToBottom() {
700        if (!isInSettings()) {
701            return mNotificationStackScroller.isScrolledToBottom();
702        }
703        return super.isScrolledToBottom();
704    }
705
706    @Override
707    protected int getMaxPanelHeight() {
708        // TODO: Figure out transition for collapsing when QS is open, adjust height here.
709        int maxPanelHeight = super.getMaxPanelHeight();
710        int emptyBottomMargin = mStackScrollerContainer.getHeight()
711                - mNotificationStackScroller.getHeight()
712                + mNotificationStackScroller.getEmptyBottomMargin();
713        int maxHeight = maxPanelHeight - emptyBottomMargin - mTopPaddingAdjustment;
714        maxHeight = Math.max(maxHeight, mStatusBarMinHeight);
715        return maxHeight;
716    }
717
718    private boolean isInSettings() {
719        return mQsExpanded;
720    }
721
722    @Override
723    protected void onHeightUpdated(float expandedHeight) {
724        if (!mQsExpanded) {
725            positionClockAndNotifications();
726        }
727        mNotificationStackScroller.setStackHeight(expandedHeight);
728        updateKeyguardHeaderVisibility();
729        updateUnlockIcon();
730    }
731
732    private void updateUnlockIcon() {
733        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
734                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
735            boolean active = getMaxPanelHeight() - getExpandedHeight() > mUnlockMoveDistance;
736            if (active && !mUnlockIconActive && mTracking) {
737                mKeyguardBottomArea.getLockIcon().animate()
738                        .alpha(1f)
739                        .scaleY(LOCK_ICON_ACTIVE_SCALE)
740                        .scaleX(LOCK_ICON_ACTIVE_SCALE)
741                        .setInterpolator(mFastOutLinearInterpolator)
742                        .setDuration(150);
743            } else if (!active && mUnlockIconActive && mTracking) {
744                mKeyguardBottomArea.getLockIcon().animate()
745                        .alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT)
746                        .scaleY(1f)
747                        .scaleX(1f)
748                        .setInterpolator(mFastOutLinearInterpolator)
749                        .setDuration(150);
750            }
751            mUnlockIconActive = active;
752        }
753    }
754
755    /**
756     * Hides the header when notifications are colliding with it.
757     */
758    private void updateKeyguardHeaderVisibility() {
759        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
760                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
761            boolean hidden = mNotificationStackScroller.getNotificationsTopY()
762                    <= mHeader.getBottom() + mNotificationsHeaderCollideDistance;
763            if (hidden && !mHeaderHidden) {
764                mHeader.animate()
765                        .alpha(0f)
766                        .withLayer()
767                        .translationY(-mHeader.getHeight()/2)
768                        .setInterpolator(mFastOutLinearInterpolator)
769                        .setDuration(200);
770            } else if (!hidden && mHeaderHidden) {
771                mHeader.animate()
772                        .alpha(1f)
773                        .withLayer()
774                        .translationY(0)
775                        .setInterpolator(mLinearOutSlowInInterpolator)
776                        .setDuration(200);
777            }
778            mHeaderHidden = hidden;
779        } else {
780            mHeader.animate().cancel();
781            mHeader.setAlpha(1f);
782            mHeader.setTranslationY(0f);
783            if (mHeader.getLayerType() != LAYER_TYPE_NONE) {
784                mHeader.setLayerType(LAYER_TYPE_NONE, null);
785            }
786            mHeaderHidden = false;
787        }
788
789    }
790
791    @Override
792    protected void onExpandingStarted() {
793        super.onExpandingStarted();
794        mNotificationStackScroller.onExpansionStarted();
795        mIsExpanding = true;
796    }
797
798    @Override
799    protected void onExpandingFinished() {
800        super.onExpandingFinished();
801        mNotificationStackScroller.onExpansionStopped();
802        mIsExpanding = false;
803    }
804
805    @Override
806    protected void onOverExpansionChanged(float overExpansion) {
807        float currentOverScroll = mNotificationStackScroller.getCurrentOverScrolledPixels(true);
808        float expansionChange = overExpansion - mOverExpansion;
809        expansionChange *= EXPANSION_RUBBER_BAND_EXTRA_FACTOR;
810        mNotificationStackScroller.setOverScrolledPixels(currentOverScroll + expansionChange,
811                true /* onTop */,
812                false /* animate */);
813        super.onOverExpansionChanged(overExpansion);
814    }
815
816    @Override
817    protected void onTrackingStarted() {
818        super.onTrackingStarted();
819        if (mStatusBar.getBarState() == StatusBarState.KEYGUARD
820                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED) {
821            mPageSwiper.animateHideLeftRightIcon();
822        }
823    }
824
825    @Override
826    protected void onTrackingStopped(boolean expand) {
827        super.onTrackingStopped(expand);
828        mOverExpansion = 0.0f;
829        mNotificationStackScroller.setOverScrolledPixels(0.0f, true /* onTop */, true /* animate */);
830        if (expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
831                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
832            mPageSwiper.showAllIcons(true);
833        }
834        if (!expand && (mStatusBar.getBarState() == StatusBarState.KEYGUARD
835                || mStatusBar.getBarState() == StatusBarState.SHADE_LOCKED)) {
836            mKeyguardBottomArea.getLockIcon().animate()
837                    .alpha(0f)
838                    .scaleX(2f)
839                    .scaleY(2f)
840                    .setInterpolator(mFastOutLinearInterpolator)
841                    .setDuration(100);
842        }
843    }
844
845    @Override
846    public void onHeightChanged(ExpandableView view) {
847        requestPanelHeightUpdate();
848    }
849
850    @Override
851    public void onScrollChanged() {
852        if (mQsExpanded) {
853            mNotificationStackScroller.setTranslationY(
854                    mNotificationTranslation - mScrollView.getScrollY());
855        }
856    }
857
858    @Override
859    protected void onConfigurationChanged(Configuration newConfig) {
860        super.onConfigurationChanged(newConfig);
861        mPageSwiper.onConfigurationChanged();
862    }
863
864    @Override
865    public void onClick(View v) {
866        if (v == mHeader.getBackgroundView()) {
867            onQsExpansionStarted();
868            if (mQsExpanded) {
869                flingSettings(0 /* vel */, false /* expand */);
870            } else if (mQsExpansionEnabled) {
871                flingSettings(0 /* vel */, true /* expand */);
872            }
873        }
874    }
875
876    @Override
877    public void onAnimationToSideStarted(boolean rightPage) {
878        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? rightPage : !rightPage;
879        if (start) {
880            mKeyguardBottomArea.launchPhone();
881        } else {
882            mKeyguardBottomArea.launchCamera();
883        }
884        mBlockTouches = true;
885    }
886
887    @Override
888    protected void onEdgeClicked(boolean right) {
889        if ((right && getRightIcon().getVisibility() != View.VISIBLE)
890                || (!right && getLeftIcon().getVisibility() != View.VISIBLE)) {
891            return;
892        }
893        mHintAnimationRunning = true;
894        mPageSwiper.startHintAnimation(right, new Runnable() {
895            @Override
896            public void run() {
897                mHintAnimationRunning = false;
898                mStatusBar.onHintFinished();
899            }
900        });
901        startHighlightIconAnimation(right ? getRightIcon() : getLeftIcon());
902        boolean start = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? right : !right;
903        if (start) {
904            mStatusBar.onPhoneHintStarted();
905        } else {
906            mStatusBar.onCameraHintStarted();
907        }
908    }
909
910    @Override
911    protected void startUnlockHintAnimation() {
912        super.startUnlockHintAnimation();
913        startHighlightIconAnimation(getCenterIcon());
914    }
915
916    /**
917     * Starts the highlight (making it fully opaque) animation on an icon.
918     */
919    private void startHighlightIconAnimation(final View icon) {
920        icon.animate()
921                .alpha(1.0f)
922                .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION)
923                .setInterpolator(mFastOutSlowInInterpolator)
924                .withEndAction(new Runnable() {
925                    @Override
926                    public void run() {
927                        icon.animate().alpha(KeyguardPageSwipeHelper.SWIPE_RESTING_ALPHA_AMOUNT)
928                                .setDuration(KeyguardPageSwipeHelper.HINT_PHASE1_DURATION)
929                                .setInterpolator(mFastOutSlowInInterpolator);
930                    }
931                });
932    }
933
934    @Override
935    public float getPageWidth() {
936        return getWidth();
937    }
938
939    @Override
940    public ArrayList<View> getTranslationViews() {
941        return mSwipeTranslationViews;
942    }
943
944    @Override
945    public View getLeftIcon() {
946        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
947                ? mKeyguardBottomArea.getCameraImageView()
948                : mKeyguardBottomArea.getPhoneImageView();
949    }
950
951    @Override
952    public View getCenterIcon() {
953        return mKeyguardBottomArea.getLockIcon();
954    }
955
956    @Override
957    public View getRightIcon() {
958        return getLayoutDirection() == LAYOUT_DIRECTION_RTL
959                ? mKeyguardBottomArea.getPhoneImageView()
960                : mKeyguardBottomArea.getCameraImageView();
961    }
962}
963