NotificationStackScrollLayout.java revision 1d49ec9f416edec68db7df30ae0476d1942f65a6
1/*
2 * Copyright (C) 2014 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.stack;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.util.AttributeSet;
24import android.util.Log;
25import android.view.MotionEvent;
26import android.view.VelocityTracker;
27import android.view.View;
28import android.view.ViewConfiguration;
29import android.view.ViewGroup;
30import android.view.ViewTreeObserver;
31import android.view.animation.AnimationUtils;
32import android.widget.OverScroller;
33
34import com.android.systemui.ExpandHelper;
35import com.android.systemui.R;
36import com.android.systemui.SwipeHelper;
37import com.android.systemui.statusbar.ActivatableNotificationView;
38import com.android.systemui.statusbar.DismissView;
39import com.android.systemui.statusbar.EmptyShadeView;
40import com.android.systemui.statusbar.ExpandableNotificationRow;
41import com.android.systemui.statusbar.ExpandableView;
42import com.android.systemui.statusbar.SpeedBumpView;
43import com.android.systemui.statusbar.policy.ScrollAdapter;
44import com.android.systemui.statusbar.stack.StackScrollState.ViewState;
45
46import java.util.ArrayList;
47import java.util.HashSet;
48
49/**
50 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
51 */
52public class NotificationStackScrollLayout extends ViewGroup
53        implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
54        ExpandableView.OnHeightChangedListener {
55
56    private static final String TAG = "NotificationStackScrollLayout";
57    private static final boolean DEBUG = false;
58    private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
59    private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
60    private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
61
62    /**
63     * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
64     */
65    private static final int INVALID_POINTER = -1;
66
67    private ExpandHelper mExpandHelper;
68    private SwipeHelper mSwipeHelper;
69    private boolean mSwipingInProgress;
70    private int mCurrentStackHeight = Integer.MAX_VALUE;
71    private int mOwnScrollY;
72    private int mMaxLayoutHeight;
73
74    private VelocityTracker mVelocityTracker;
75    private OverScroller mScroller;
76    private int mTouchSlop;
77    private int mMinimumVelocity;
78    private int mMaximumVelocity;
79    private int mOverflingDistance;
80    private float mMaxOverScroll;
81    private boolean mIsBeingDragged;
82    private int mLastMotionY;
83    private int mDownX;
84    private int mActivePointerId;
85
86    private int mSidePaddings;
87    private Paint mDebugPaint;
88    private int mContentHeight;
89    private int mCollapsedSize;
90    private int mBottomStackSlowDownHeight;
91    private int mBottomStackPeekSize;
92    private int mPaddingBetweenElements;
93    private int mPaddingBetweenElementsDimmed;
94    private int mPaddingBetweenElementsNormal;
95    private int mTopPadding;
96    private int mCollapseSecondCardPadding;
97
98    /**
99     * The algorithm which calculates the properties for our children
100     */
101    private StackScrollAlgorithm mStackScrollAlgorithm;
102
103    /**
104     * The current State this Layout is in
105     */
106    private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
107    private AmbientState mAmbientState = new AmbientState();
108    private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>();
109    private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
110    private ArrayList<View> mSnappedBackChildren = new ArrayList<View>();
111    private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>();
112    private ArrayList<View> mChildrenChangingPositions = new ArrayList<View>();
113    private HashSet<View> mFromMoreCardAdditions = new HashSet<>();
114    private ArrayList<AnimationEvent> mAnimationEvents
115            = new ArrayList<AnimationEvent>();
116    private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
117    private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
118    private boolean mAnimationsEnabled;
119    private boolean mChangePositionInProgress;
120
121    /**
122     * The raw amount of the overScroll on the top, which is not rubber-banded.
123     */
124    private float mOverScrolledTopPixels;
125
126    /**
127     * The raw amount of the overScroll on the bottom, which is not rubber-banded.
128     */
129    private float mOverScrolledBottomPixels;
130
131    private OnChildLocationsChangedListener mListener;
132    private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
133    private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
134    private boolean mNeedsAnimation;
135    private boolean mTopPaddingNeedsAnimation;
136    private boolean mDimmedNeedsAnimation;
137    private boolean mHideSensitiveNeedsAnimation;
138    private boolean mDarkNeedsAnimation;
139    private boolean mActivateNeedsAnimation;
140    private boolean mGoToFullShadeNeedsAnimation;
141    private boolean mIsExpanded = true;
142    private boolean mChildrenUpdateRequested;
143    private SpeedBumpView mSpeedBumpView;
144    private boolean mIsExpansionChanging;
145    private boolean mExpandingNotification;
146    private boolean mExpandedInThisMotion;
147    private boolean mScrollingEnabled;
148    private DismissView mDismissView;
149    private EmptyShadeView mEmptyShadeView;
150    private boolean mDismissAllInProgress;
151
152    /**
153     * Was the scroller scrolled to the top when the down motion was observed?
154     */
155    private boolean mScrolledToTopOnFirstDown;
156
157    /**
158     * The minimal amount of over scroll which is needed in order to switch to the quick settings
159     * when over scrolling on a expanded card.
160     */
161    private float mMinTopOverScrollToEscape;
162    private int mIntrinsicPadding;
163    private int mNotificationTopPadding;
164    private float mTopPaddingOverflow;
165    private boolean mDontReportNextOverScroll;
166    private boolean mRequestViewResizeAnimationOnLayout;
167    private boolean mNeedViewResizeAnimation;
168
169    /**
170     * The maximum scrollPosition which we are allowed to reach when a notification was expanded.
171     * This is needed to avoid scrolling too far after the notification was collapsed in the same
172     * motion.
173     */
174    private int mMaxScrollAfterExpand;
175    private SwipeHelper.LongPressListener mLongPressListener;
176
177    /**
178     * Should in this touch motion only be scrolling allowed? It's true when the scroller was
179     * animating.
180     */
181    private boolean mOnlyScrollingInThisMotion;
182    private ViewGroup mScrollView;
183    private boolean mInterceptDelegateEnabled;
184    private boolean mDelegateToScrollView;
185    private boolean mDisallowScrollingInThisMotion;
186    private long mGoToFullShadeDelay;
187
188    private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
189            = new ViewTreeObserver.OnPreDrawListener() {
190        @Override
191        public boolean onPreDraw() {
192            updateChildren();
193            mChildrenUpdateRequested = false;
194            getViewTreeObserver().removeOnPreDrawListener(this);
195            return true;
196        }
197    };
198
199    public NotificationStackScrollLayout(Context context) {
200        this(context, null);
201    }
202
203    public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
204        this(context, attrs, 0);
205    }
206
207    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
208        this(context, attrs, defStyleAttr, 0);
209    }
210
211    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
212            int defStyleRes) {
213        super(context, attrs, defStyleAttr, defStyleRes);
214        int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
215        int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height);
216        mExpandHelper = new ExpandHelper(getContext(), this,
217                minHeight, maxHeight);
218        mExpandHelper.setEventSource(this);
219        mExpandHelper.setScrollAdapter(this);
220
221        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, getContext());
222        mSwipeHelper.setLongPressListener(mLongPressListener);
223        initView(context);
224        if (DEBUG) {
225            setWillNotDraw(false);
226            mDebugPaint = new Paint();
227            mDebugPaint.setColor(0xffff0000);
228            mDebugPaint.setStrokeWidth(2);
229            mDebugPaint.setStyle(Paint.Style.STROKE);
230        }
231    }
232
233    @Override
234    protected void onDraw(Canvas canvas) {
235        if (DEBUG) {
236            int y = mCollapsedSize;
237            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
238            y = (int) (getLayoutHeight() - mBottomStackPeekSize
239                    - mBottomStackSlowDownHeight);
240            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
241            y = (int) (getLayoutHeight() - mBottomStackPeekSize);
242            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
243            y = (int) getLayoutHeight();
244            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
245            y = getHeight() - getEmptyBottomMargin();
246            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
247        }
248    }
249
250    private void initView(Context context) {
251        mScroller = new OverScroller(getContext());
252        setFocusable(true);
253        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
254        setClipChildren(false);
255        final ViewConfiguration configuration = ViewConfiguration.get(context);
256        mTouchSlop = configuration.getScaledTouchSlop();
257        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
258        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
259        mOverflingDistance = configuration.getScaledOverflingDistance();
260
261        mSidePaddings = context.getResources()
262                .getDimensionPixelSize(R.dimen.notification_side_padding);
263        mCollapsedSize = context.getResources()
264                .getDimensionPixelSize(R.dimen.notification_min_height);
265        mBottomStackPeekSize = context.getResources()
266                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
267        mStackScrollAlgorithm = new StackScrollAlgorithm(context);
268        mStackScrollAlgorithm.setDimmed(mAmbientState.isDimmed());
269        mPaddingBetweenElementsDimmed = context.getResources()
270                .getDimensionPixelSize(R.dimen.notification_padding_dimmed);
271        mPaddingBetweenElementsNormal = context.getResources()
272                .getDimensionPixelSize(R.dimen.notification_padding);
273        updatePadding(mAmbientState.isDimmed());
274        mMinTopOverScrollToEscape = getResources().getDimensionPixelSize(
275                R.dimen.min_top_overscroll_to_qs);
276        mNotificationTopPadding = getResources().getDimensionPixelSize(
277                R.dimen.notifications_top_padding);
278        mCollapseSecondCardPadding = getResources().getDimensionPixelSize(
279                R.dimen.notification_collapse_second_card_padding);
280    }
281
282    private void updatePadding(boolean dimmed) {
283        mPaddingBetweenElements = dimmed && mStackScrollAlgorithm.shouldScaleDimmed()
284                ? mPaddingBetweenElementsDimmed
285                : mPaddingBetweenElementsNormal;
286        mBottomStackSlowDownHeight = mStackScrollAlgorithm.getBottomStackSlowDownLength();
287        updateContentHeight();
288        notifyHeightChangeListener(null);
289    }
290
291    private void notifyHeightChangeListener(ExpandableView view) {
292        if (mOnHeightChangedListener != null) {
293            mOnHeightChangedListener.onHeightChanged(view);
294        }
295    }
296
297    @Override
298    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
299        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
300        int mode = MeasureSpec.getMode(widthMeasureSpec);
301        int size = MeasureSpec.getSize(widthMeasureSpec);
302        int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode);
303        measureChildren(childMeasureSpec, heightMeasureSpec);
304    }
305
306    @Override
307    protected void onLayout(boolean changed, int l, int t, int r, int b) {
308
309        // we layout all our children centered on the top
310        float centerX = getWidth() / 2.0f;
311        for (int i = 0; i < getChildCount(); i++) {
312            View child = getChildAt(i);
313            float width = child.getMeasuredWidth();
314            float height = child.getMeasuredHeight();
315            child.layout((int) (centerX - width / 2.0f),
316                    0,
317                    (int) (centerX + width / 2.0f),
318                    (int) height);
319        }
320        setMaxLayoutHeight(getHeight());
321        updateContentHeight();
322        clampScrollPosition();
323        requestAnimationOnViewResize();
324        requestChildrenUpdate();
325    }
326
327    private void requestAnimationOnViewResize() {
328        if (mRequestViewResizeAnimationOnLayout && mIsExpanded && mAnimationsEnabled) {
329            mNeedViewResizeAnimation = true;
330            mNeedsAnimation = true;
331        }
332        mRequestViewResizeAnimationOnLayout = false;
333    }
334
335    public void updateSpeedBumpIndex(int newIndex) {
336        int currentIndex = indexOfChild(mSpeedBumpView);
337
338        // If we are currently layouted before the new speed bump index, we have to decrease it.
339        boolean validIndex = newIndex > 0;
340        if (newIndex > getChildCount() - 1) {
341            validIndex = false;
342            newIndex = -1;
343        }
344        if (validIndex && currentIndex != newIndex) {
345            changeViewPosition(mSpeedBumpView, newIndex);
346        }
347        updateSpeedBump(validIndex);
348        mAmbientState.setSpeedBumpIndex(newIndex);
349    }
350
351    public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
352        mListener = listener;
353    }
354
355    /**
356     * Returns the location the given child is currently rendered at.
357     *
358     * @param child the child to get the location for
359     * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants
360     */
361    public int getChildLocation(View child) {
362        ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child);
363        if (childViewState == null) {
364            return ViewState.LOCATION_UNKNOWN;
365        }
366        return childViewState.location;
367    }
368
369    private void setMaxLayoutHeight(int maxLayoutHeight) {
370        mMaxLayoutHeight = maxLayoutHeight;
371        updateAlgorithmHeightAndPadding();
372    }
373
374    private void updateAlgorithmHeightAndPadding() {
375        mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
376        mStackScrollAlgorithm.setTopPadding(mTopPadding);
377    }
378
379    /**
380     * @return whether the height of the layout needs to be adapted, in order to ensure that the
381     *         last child is not in the bottom stack.
382     */
383    private boolean needsHeightAdaption() {
384        return getNotGoneChildCount() > 1;
385    }
386
387    /**
388     * Updates the children views according to the stack scroll algorithm. Call this whenever
389     * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
390     */
391    private void updateChildren() {
392        mAmbientState.setScrollY(mOwnScrollY);
393        mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
394        if (!isCurrentlyAnimating() && !mNeedsAnimation) {
395            applyCurrentState();
396        } else {
397            startAnimationToState();
398        }
399    }
400
401    private void requestChildrenUpdate() {
402        if (!mChildrenUpdateRequested) {
403            getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
404            mChildrenUpdateRequested = true;
405            invalidate();
406        }
407    }
408
409    private boolean isCurrentlyAnimating() {
410        return mStateAnimator.isRunning();
411    }
412
413    private void clampScrollPosition() {
414        int scrollRange = getScrollRange();
415        if (scrollRange < mOwnScrollY) {
416            mOwnScrollY = scrollRange;
417        }
418    }
419
420    public int getTopPadding() {
421        return mTopPadding;
422    }
423
424    private void setTopPadding(int topPadding, boolean animate) {
425        if (mTopPadding != topPadding) {
426            mTopPadding = topPadding;
427            updateAlgorithmHeightAndPadding();
428            updateContentHeight();
429            if (animate && mAnimationsEnabled && mIsExpanded) {
430                mTopPaddingNeedsAnimation = true;
431                mNeedsAnimation =  true;
432            }
433            requestChildrenUpdate();
434            notifyHeightChangeListener(null);
435        }
436    }
437
438    /**
439     * Update the height of the stack to a new height.
440     *
441     * @param height the new height of the stack
442     */
443    public void setStackHeight(float height) {
444        setIsExpanded(height > 0.0f);
445        int newStackHeight = (int) height;
446        int minStackHeight = getMinStackHeight();
447        int stackHeight;
448        if (newStackHeight - mTopPadding >= minStackHeight || getNotGoneChildCount() == 0) {
449            setTranslationY(mTopPaddingOverflow);
450            stackHeight = newStackHeight;
451        } else {
452
453            // We did not reach the position yet where we actually start growing,
454            // so we translate the stack upwards.
455            int translationY = (newStackHeight - minStackHeight);
456            // A slight parallax effect is introduced in order for the stack to catch up with
457            // the top card.
458            float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight;
459            partiallyThere = Math.max(0, partiallyThere);
460            translationY += (1 - partiallyThere) * (mBottomStackPeekSize +
461                    mCollapseSecondCardPadding);
462            setTranslationY(translationY - mTopPadding);
463            stackHeight = (int) (height - (translationY - mTopPadding));
464        }
465        if (stackHeight != mCurrentStackHeight) {
466            mCurrentStackHeight = stackHeight;
467            updateAlgorithmHeightAndPadding();
468            requestChildrenUpdate();
469        }
470    }
471
472    /**
473     * Get the current height of the view. This is at most the msize of the view given by a the
474     * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
475     *
476     * @return either the layout height or the externally defined height, whichever is smaller
477     */
478    private int getLayoutHeight() {
479        return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
480    }
481
482    public int getItemHeight() {
483        return mCollapsedSize;
484    }
485
486    public int getBottomStackPeekSize() {
487        return mBottomStackPeekSize;
488    }
489
490    public int getCollapseSecondCardPadding() {
491        return mCollapseSecondCardPadding;
492    }
493
494    public void setLongPressListener(SwipeHelper.LongPressListener listener) {
495        mSwipeHelper.setLongPressListener(listener);
496        mLongPressListener = listener;
497    }
498
499    public void setScrollView(ViewGroup scrollView) {
500        mScrollView = scrollView;
501    }
502
503    public void setInterceptDelegateEnabled(boolean interceptDelegateEnabled) {
504        mInterceptDelegateEnabled = interceptDelegateEnabled;
505    }
506
507    public void onChildDismissed(View v) {
508        if (mDismissAllInProgress) {
509            return;
510        }
511        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
512        final View veto = v.findViewById(R.id.veto);
513        if (veto != null && veto.getVisibility() != View.GONE) {
514            veto.performClick();
515        }
516        setSwipingInProgress(false);
517        if (mDragAnimPendingChildren.contains(v)) {
518            // We start the swipe and finish it in the same frame, we don't want any animation
519            // for the drag
520            mDragAnimPendingChildren.remove(v);
521        }
522        mSwipedOutViews.add(v);
523        mAmbientState.onDragFinished(v);
524    }
525
526    @Override
527    public void onChildSnappedBack(View animView) {
528        mAmbientState.onDragFinished(animView);
529        if (!mDragAnimPendingChildren.contains(animView)) {
530            if (mAnimationsEnabled) {
531                mSnappedBackChildren.add(animView);
532                mNeedsAnimation = true;
533            }
534            requestChildrenUpdate();
535        } else {
536            // We start the swipe and snap back in the same frame, we don't want any animation
537            mDragAnimPendingChildren.remove(animView);
538        }
539    }
540
541    @Override
542    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
543        return false;
544    }
545
546    public void onBeginDrag(View v) {
547        setSwipingInProgress(true);
548        mAmbientState.onBeginDrag(v);
549        if (mAnimationsEnabled) {
550            mDragAnimPendingChildren.add(v);
551            mNeedsAnimation = true;
552        }
553        requestChildrenUpdate();
554    }
555
556    public void onDragCancelled(View v) {
557        setSwipingInProgress(false);
558    }
559
560    public View getChildAtPosition(MotionEvent ev) {
561        return getChildAtPosition(ev.getX(), ev.getY());
562    }
563
564    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
565        int[] location = new int[2];
566        getLocationOnScreen(location);
567        return getChildAtPosition(touchX - location[0], touchY - location[1]);
568    }
569
570    public ExpandableView getChildAtPosition(float touchX, float touchY) {
571        // find the view under the pointer, accounting for GONE views
572        final int count = getChildCount();
573        for (int childIdx = 0; childIdx < count; childIdx++) {
574            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
575            if (slidingChild.getVisibility() == GONE) {
576                continue;
577            }
578            float childTop = slidingChild.getTranslationY();
579            float top = childTop + slidingChild.getClipTopAmount();
580            float bottom = top + slidingChild.getActualHeight();
581
582            // Allow the full width of this view to prevent gesture conflict on Keyguard (phone and
583            // camera affordance).
584            int left = 0;
585            int right = getWidth();
586
587            if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
588                return slidingChild;
589            }
590        }
591        return null;
592    }
593
594    public boolean canChildBeExpanded(View v) {
595        return v instanceof ExpandableNotificationRow
596                && ((ExpandableNotificationRow) v).isExpandable();
597    }
598
599    public void setUserExpandedChild(View v, boolean userExpanded) {
600        if (v instanceof ExpandableNotificationRow) {
601            ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
602        }
603    }
604
605    public void setUserLockedChild(View v, boolean userLocked) {
606        if (v instanceof ExpandableNotificationRow) {
607            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
608        }
609        removeLongPressCallback();
610        requestDisallowInterceptTouchEvent(true);
611    }
612
613    @Override
614    public void expansionStateChanged(boolean isExpanding) {
615        mExpandingNotification = isExpanding;
616        if (!mExpandedInThisMotion) {
617            mMaxScrollAfterExpand = mOwnScrollY;
618            mExpandedInThisMotion = true;
619        }
620    }
621
622    public void setScrollingEnabled(boolean enable) {
623        mScrollingEnabled = enable;
624    }
625
626    public void setExpandingEnabled(boolean enable) {
627        mExpandHelper.setEnabled(enable);
628    }
629
630    private boolean isScrollingEnabled() {
631        return mScrollingEnabled;
632    }
633
634    public View getChildContentView(View v) {
635        return v;
636    }
637
638    public boolean canChildBeDismissed(View v) {
639        final View veto = v.findViewById(R.id.veto);
640        return (veto != null && veto.getVisibility() != View.GONE);
641    }
642
643    private void setSwipingInProgress(boolean isSwiped) {
644        mSwipingInProgress = isSwiped;
645        if(isSwiped) {
646            requestDisallowInterceptTouchEvent(true);
647        }
648    }
649
650    @Override
651    protected void onConfigurationChanged(Configuration newConfig) {
652        super.onConfigurationChanged(newConfig);
653        float densityScale = getResources().getDisplayMetrics().density;
654        mSwipeHelper.setDensityScale(densityScale);
655        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
656        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
657        initView(getContext());
658    }
659
660    public void dismissViewAnimated(View child, Runnable endRunnable, int delay, long duration) {
661        child.setClipBounds(null);
662        mSwipeHelper.dismissChild(child, 0, endRunnable, delay, true, duration);
663    }
664
665    @Override
666    public boolean onTouchEvent(MotionEvent ev) {
667        boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
668                || ev.getActionMasked()== MotionEvent.ACTION_UP;
669        if (mDelegateToScrollView) {
670            if (isCancelOrUp) {
671                mDelegateToScrollView = false;
672            }
673            transformTouchEvent(ev, this, mScrollView);
674            return mScrollView.onTouchEvent(ev);
675        }
676        boolean expandWantsIt = false;
677        if (!mSwipingInProgress && !mOnlyScrollingInThisMotion && isScrollingEnabled()) {
678            if (isCancelOrUp) {
679                mExpandHelper.onlyObserveMovements(false);
680            }
681            boolean wasExpandingBefore = mExpandingNotification;
682            expandWantsIt = mExpandHelper.onTouchEvent(ev);
683            if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore
684                    && !mDisallowScrollingInThisMotion) {
685                dispatchDownEventToScroller(ev);
686            }
687        }
688        boolean scrollerWantsIt = false;
689        if (!mSwipingInProgress && !mExpandingNotification && !mDisallowScrollingInThisMotion) {
690            scrollerWantsIt = onScrollTouch(ev);
691        }
692        boolean horizontalSwipeWantsIt = false;
693        if (!mIsBeingDragged
694                && !mExpandingNotification
695                && !mExpandedInThisMotion
696                && !mOnlyScrollingInThisMotion) {
697            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
698        }
699        return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev);
700    }
701
702    private void dispatchDownEventToScroller(MotionEvent ev) {
703        MotionEvent downEvent = MotionEvent.obtain(ev);
704        downEvent.setAction(MotionEvent.ACTION_DOWN);
705        onScrollTouch(downEvent);
706        downEvent.recycle();
707    }
708
709    private boolean onScrollTouch(MotionEvent ev) {
710        if (!isScrollingEnabled()) {
711            return false;
712        }
713        initVelocityTrackerIfNotExists();
714        mVelocityTracker.addMovement(ev);
715
716        final int action = ev.getAction();
717
718        switch (action & MotionEvent.ACTION_MASK) {
719            case MotionEvent.ACTION_DOWN: {
720                if (getChildCount() == 0 || !isInContentBounds(ev)) {
721                    return false;
722                }
723                boolean isBeingDragged = !mScroller.isFinished();
724                setIsBeingDragged(isBeingDragged);
725
726                /*
727                 * If being flinged and user touches, stop the fling. isFinished
728                 * will be false if being flinged.
729                 */
730                if (!mScroller.isFinished()) {
731                    mScroller.forceFinished(true);
732                }
733
734                // Remember where the motion event started
735                mLastMotionY = (int) ev.getY();
736                mDownX = (int) ev.getX();
737                mActivePointerId = ev.getPointerId(0);
738                break;
739            }
740            case MotionEvent.ACTION_MOVE:
741                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
742                if (activePointerIndex == -1) {
743                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
744                    break;
745                }
746
747                final int y = (int) ev.getY(activePointerIndex);
748                final int x = (int) ev.getX(activePointerIndex);
749                int deltaY = mLastMotionY - y;
750                final int xDiff = Math.abs(x - mDownX);
751                final int yDiff = Math.abs(deltaY);
752                if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) {
753                    setIsBeingDragged(true);
754                    if (deltaY > 0) {
755                        deltaY -= mTouchSlop;
756                    } else {
757                        deltaY += mTouchSlop;
758                    }
759                }
760                if (mIsBeingDragged) {
761                    // Scroll to follow the motion event
762                    mLastMotionY = y;
763                    int range = getScrollRange();
764                    if (mExpandedInThisMotion) {
765                        range = Math.min(range, mMaxScrollAfterExpand);
766                    }
767
768                    float scrollAmount;
769                    if (deltaY < 0) {
770                        scrollAmount = overScrollDown(deltaY);
771                    } else {
772                        scrollAmount = overScrollUp(deltaY, range);
773                    }
774
775                    // Calling overScrollBy will call onOverScrolled, which
776                    // calls onScrollChanged if applicable.
777                    if (scrollAmount != 0.0f) {
778                        // The scrolling motion could not be compensated with the
779                        // existing overScroll, we have to scroll the view
780                        overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY,
781                                0, range, 0, getHeight() / 2, true);
782                    }
783                }
784                break;
785            case MotionEvent.ACTION_UP:
786                if (mIsBeingDragged) {
787                    final VelocityTracker velocityTracker = mVelocityTracker;
788                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
789                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
790
791                    if (shouldOverScrollFling(initialVelocity)) {
792                        onOverScrollFling(true, initialVelocity);
793                    } else {
794                        if (getChildCount() > 0) {
795                            if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
796                                float currentOverScrollTop = getCurrentOverScrollAmount(true);
797                                if (currentOverScrollTop == 0.0f || initialVelocity > 0) {
798                                    fling(-initialVelocity);
799                                } else {
800                                    onOverScrollFling(false, initialVelocity);
801                                }
802                            } else {
803                                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
804                                        getScrollRange())) {
805                                    postInvalidateOnAnimation();
806                                }
807                            }
808                        }
809                    }
810
811                    mActivePointerId = INVALID_POINTER;
812                    endDrag();
813                }
814
815                break;
816            case MotionEvent.ACTION_CANCEL:
817                if (mIsBeingDragged && getChildCount() > 0) {
818                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
819                        postInvalidateOnAnimation();
820                    }
821                    mActivePointerId = INVALID_POINTER;
822                    endDrag();
823                }
824                break;
825            case MotionEvent.ACTION_POINTER_DOWN: {
826                final int index = ev.getActionIndex();
827                mLastMotionY = (int) ev.getY(index);
828                mDownX = (int) ev.getX(index);
829                mActivePointerId = ev.getPointerId(index);
830                break;
831            }
832            case MotionEvent.ACTION_POINTER_UP:
833                onSecondaryPointerUp(ev);
834                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
835                mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
836                break;
837        }
838        return true;
839    }
840
841    private void onOverScrollFling(boolean open, int initialVelocity) {
842        if (mOverscrollTopChangedListener != null) {
843            mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open);
844        }
845        mDontReportNextOverScroll = true;
846        setOverScrollAmount(0.0f, true, false);
847    }
848
849    /**
850     * Perform a scroll upwards and adapt the overscroll amounts accordingly
851     *
852     * @param deltaY The amount to scroll upwards, has to be positive.
853     * @return The amount of scrolling to be performed by the scroller,
854     *         not handled by the overScroll amount.
855     */
856    private float overScrollUp(int deltaY, int range) {
857        deltaY = Math.max(deltaY, 0);
858        float currentTopAmount = getCurrentOverScrollAmount(true);
859        float newTopAmount = currentTopAmount - deltaY;
860        if (currentTopAmount > 0) {
861            setOverScrollAmount(newTopAmount, true /* onTop */,
862                    false /* animate */);
863        }
864        // Top overScroll might not grab all scrolling motion,
865        // we have to scroll as well.
866        float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
867        float newScrollY = mOwnScrollY + scrollAmount;
868        if (newScrollY > range) {
869            if (!mExpandedInThisMotion) {
870                float currentBottomPixels = getCurrentOverScrolledPixels(false);
871                // We overScroll on the top
872                setOverScrolledPixels(currentBottomPixels + newScrollY - range,
873                        false /* onTop */,
874                        false /* animate */);
875            }
876            mOwnScrollY = range;
877            scrollAmount = 0.0f;
878        }
879        return scrollAmount;
880    }
881
882    /**
883     * Perform a scroll downward and adapt the overscroll amounts accordingly
884     *
885     * @param deltaY The amount to scroll downwards, has to be negative.
886     * @return The amount of scrolling to be performed by the scroller,
887     *         not handled by the overScroll amount.
888     */
889    private float overScrollDown(int deltaY) {
890        deltaY = Math.min(deltaY, 0);
891        float currentBottomAmount = getCurrentOverScrollAmount(false);
892        float newBottomAmount = currentBottomAmount + deltaY;
893        if (currentBottomAmount > 0) {
894            setOverScrollAmount(newBottomAmount, false /* onTop */,
895                    false /* animate */);
896        }
897        // Bottom overScroll might not grab all scrolling motion,
898        // we have to scroll as well.
899        float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
900        float newScrollY = mOwnScrollY + scrollAmount;
901        if (newScrollY < 0) {
902            float currentTopPixels = getCurrentOverScrolledPixels(true);
903            // We overScroll on the top
904            setOverScrolledPixels(currentTopPixels - newScrollY,
905                    true /* onTop */,
906                    false /* animate */);
907            mOwnScrollY = 0;
908            scrollAmount = 0.0f;
909        }
910        return scrollAmount;
911    }
912
913    private void onSecondaryPointerUp(MotionEvent ev) {
914        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
915                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
916        final int pointerId = ev.getPointerId(pointerIndex);
917        if (pointerId == mActivePointerId) {
918            // This was our active pointer going up. Choose a new
919            // active pointer and adjust accordingly.
920            // TODO: Make this decision more intelligent.
921            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
922            mLastMotionY = (int) ev.getY(newPointerIndex);
923            mActivePointerId = ev.getPointerId(newPointerIndex);
924            if (mVelocityTracker != null) {
925                mVelocityTracker.clear();
926            }
927        }
928    }
929
930    private void initVelocityTrackerIfNotExists() {
931        if (mVelocityTracker == null) {
932            mVelocityTracker = VelocityTracker.obtain();
933        }
934    }
935
936    private void recycleVelocityTracker() {
937        if (mVelocityTracker != null) {
938            mVelocityTracker.recycle();
939            mVelocityTracker = null;
940        }
941    }
942
943    private void initOrResetVelocityTracker() {
944        if (mVelocityTracker == null) {
945            mVelocityTracker = VelocityTracker.obtain();
946        } else {
947            mVelocityTracker.clear();
948        }
949    }
950
951    @Override
952    public void computeScroll() {
953        if (mScroller.computeScrollOffset()) {
954            // This is called at drawing time by ViewGroup.
955            int oldX = mScrollX;
956            int oldY = mOwnScrollY;
957            int x = mScroller.getCurrX();
958            int y = mScroller.getCurrY();
959
960            if (oldX != x || oldY != y) {
961                final int range = getScrollRange();
962                if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
963                    float currVelocity = mScroller.getCurrVelocity();
964                    if (currVelocity >= mMinimumVelocity) {
965                        mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
966                    }
967                }
968
969                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
970                        0, (int) (mMaxOverScroll), false);
971                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
972            }
973
974            // Keep on drawing until the animation has finished.
975            postInvalidateOnAnimation();
976        }
977    }
978
979    @Override
980    protected boolean overScrollBy(int deltaX, int deltaY,
981            int scrollX, int scrollY,
982            int scrollRangeX, int scrollRangeY,
983            int maxOverScrollX, int maxOverScrollY,
984            boolean isTouchEvent) {
985
986        int newScrollY = scrollY + deltaY;
987
988        final int top = -maxOverScrollY;
989        final int bottom = maxOverScrollY + scrollRangeY;
990
991        boolean clampedY = false;
992        if (newScrollY > bottom) {
993            newScrollY = bottom;
994            clampedY = true;
995        } else if (newScrollY < top) {
996            newScrollY = top;
997            clampedY = true;
998        }
999
1000        onOverScrolled(0, newScrollY, false, clampedY);
1001
1002        return clampedY;
1003    }
1004
1005    /**
1006     * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded
1007     * overscroll effect based on numPixels. By default this will also cancel animations on the
1008     * same overScroll edge.
1009     *
1010     * @param numPixels The amount of pixels to overScroll by. These will be scaled according to
1011     *                  the rubber-banding logic.
1012     * @param onTop Should the effect be applied on top of the scroller.
1013     * @param animate Should an animation be performed.
1014     */
1015    public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) {
1016        setOverScrollAmount(numPixels * getRubberBandFactor(onTop), onTop, animate, true);
1017    }
1018
1019    /**
1020     * Set the effective overScroll amount which will be directly reflected in the layout.
1021     * By default this will also cancel animations on the same overScroll edge.
1022     *
1023     * @param amount The amount to overScroll by.
1024     * @param onTop Should the effect be applied on top of the scroller.
1025     * @param animate Should an animation be performed.
1026     */
1027    public void setOverScrollAmount(float amount, boolean onTop, boolean animate) {
1028        setOverScrollAmount(amount, onTop, animate, true);
1029    }
1030
1031    /**
1032     * Set the effective overScroll amount which will be directly reflected in the layout.
1033     *
1034     * @param amount The amount to overScroll by.
1035     * @param onTop Should the effect be applied on top of the scroller.
1036     * @param animate Should an animation be performed.
1037     * @param cancelAnimators Should running animations be cancelled.
1038     */
1039    public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
1040            boolean cancelAnimators) {
1041        setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop));
1042    }
1043
1044    /**
1045     * Set the effective overScroll amount which will be directly reflected in the layout.
1046     *
1047     * @param amount The amount to overScroll by.
1048     * @param onTop Should the effect be applied on top of the scroller.
1049     * @param animate Should an animation be performed.
1050     * @param cancelAnimators Should running animations be cancelled.
1051     * @param isRubberbanded The value which will be passed to
1052     *                     {@link OnOverscrollTopChangedListener#onOverscrollTopChanged}
1053     */
1054    public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
1055            boolean cancelAnimators, boolean isRubberbanded) {
1056        if (cancelAnimators) {
1057            mStateAnimator.cancelOverScrollAnimators(onTop);
1058        }
1059        setOverScrollAmountInternal(amount, onTop, animate, isRubberbanded);
1060    }
1061
1062    private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate,
1063            boolean isRubberbanded) {
1064        amount = Math.max(0, amount);
1065        if (animate) {
1066            mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded);
1067        } else {
1068            setOverScrolledPixels(amount / getRubberBandFactor(onTop), onTop);
1069            mAmbientState.setOverScrollAmount(amount, onTop);
1070            if (onTop) {
1071                notifyOverscrollTopListener(amount, isRubberbanded);
1072            }
1073            requestChildrenUpdate();
1074        }
1075    }
1076
1077    private void notifyOverscrollTopListener(float amount, boolean isRubberbanded) {
1078        mExpandHelper.onlyObserveMovements(amount > 1.0f);
1079        if (mDontReportNextOverScroll) {
1080            mDontReportNextOverScroll = false;
1081            return;
1082        }
1083        if (mOverscrollTopChangedListener != null) {
1084            mOverscrollTopChangedListener.onOverscrollTopChanged(amount, isRubberbanded);
1085        }
1086    }
1087
1088    public void setOverscrollTopChangedListener(
1089            OnOverscrollTopChangedListener overscrollTopChangedListener) {
1090        mOverscrollTopChangedListener = overscrollTopChangedListener;
1091    }
1092
1093    public float getCurrentOverScrollAmount(boolean top) {
1094        return mAmbientState.getOverScrollAmount(top);
1095    }
1096
1097    public float getCurrentOverScrolledPixels(boolean top) {
1098        return top? mOverScrolledTopPixels : mOverScrolledBottomPixels;
1099    }
1100
1101    private void setOverScrolledPixels(float amount, boolean onTop) {
1102        if (onTop) {
1103            mOverScrolledTopPixels = amount;
1104        } else {
1105            mOverScrolledBottomPixels = amount;
1106        }
1107    }
1108
1109    private void customScrollTo(int y) {
1110        mOwnScrollY = y;
1111        updateChildren();
1112    }
1113
1114    @Override
1115    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
1116        // Treat animating scrolls differently; see #computeScroll() for why.
1117        if (!mScroller.isFinished()) {
1118            final int oldX = mScrollX;
1119            final int oldY = mOwnScrollY;
1120            mScrollX = scrollX;
1121            mOwnScrollY = scrollY;
1122            if (clampedY) {
1123                springBack();
1124            } else {
1125                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
1126                invalidateParentIfNeeded();
1127                updateChildren();
1128                float overScrollTop = getCurrentOverScrollAmount(true);
1129                if (mOwnScrollY < 0) {
1130                    notifyOverscrollTopListener(-mOwnScrollY, isRubberbanded(true));
1131                } else {
1132                    notifyOverscrollTopListener(overScrollTop, isRubberbanded(true));
1133                }
1134            }
1135        } else {
1136            customScrollTo(scrollY);
1137            scrollTo(scrollX, mScrollY);
1138        }
1139    }
1140
1141    private void springBack() {
1142        int scrollRange = getScrollRange();
1143        boolean overScrolledTop = mOwnScrollY <= 0;
1144        boolean overScrolledBottom = mOwnScrollY >= scrollRange;
1145        if (overScrolledTop || overScrolledBottom) {
1146            boolean onTop;
1147            float newAmount;
1148            if (overScrolledTop) {
1149                onTop = true;
1150                newAmount = -mOwnScrollY;
1151                mOwnScrollY = 0;
1152                mDontReportNextOverScroll = true;
1153            } else {
1154                onTop = false;
1155                newAmount = mOwnScrollY - scrollRange;
1156                mOwnScrollY = scrollRange;
1157            }
1158            setOverScrollAmount(newAmount, onTop, false);
1159            setOverScrollAmount(0.0f, onTop, true);
1160            mScroller.forceFinished(true);
1161        }
1162    }
1163
1164    private int getScrollRange() {
1165        int scrollRange = 0;
1166        ExpandableView firstChild = (ExpandableView) getFirstChildNotGone();
1167        if (firstChild != null) {
1168            int contentHeight = getContentHeight();
1169            int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild);
1170            scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize
1171                    + mBottomStackSlowDownHeight);
1172            if (scrollRange > 0) {
1173                View lastChild = getLastChildNotGone();
1174                // We want to at least be able collapse the first item and not ending in a weird
1175                // end state.
1176                scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize);
1177            }
1178        }
1179        return scrollRange;
1180    }
1181
1182    /**
1183     * @return the first child which has visibility unequal to GONE
1184     */
1185    private View getFirstChildNotGone() {
1186        int childCount = getChildCount();
1187        for (int i = 0; i < childCount; i++) {
1188            View child = getChildAt(i);
1189            if (child.getVisibility() != View.GONE) {
1190                return child;
1191            }
1192        }
1193        return null;
1194    }
1195
1196    /**
1197     * @return The first child which has visibility unequal to GONE which is currently below the
1198     *         given translationY or equal to it.
1199     */
1200    private View getFirstChildBelowTranlsationY(float translationY) {
1201        int childCount = getChildCount();
1202        for (int i = 0; i < childCount; i++) {
1203            View child = getChildAt(i);
1204            if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) {
1205                return child;
1206            }
1207        }
1208        return null;
1209    }
1210
1211    /**
1212     * @return the last child which has visibility unequal to GONE
1213     */
1214    public View getLastChildNotGone() {
1215        int childCount = getChildCount();
1216        for (int i = childCount - 1; i >= 0; i--) {
1217            View child = getChildAt(i);
1218            if (child.getVisibility() != View.GONE) {
1219                return child;
1220            }
1221        }
1222        return null;
1223    }
1224
1225    /**
1226     * @return the number of children which have visibility unequal to GONE
1227     */
1228    public int getNotGoneChildCount() {
1229        int childCount = getChildCount();
1230        int count = 0;
1231        for (int i = 0; i < childCount; i++) {
1232            View child = getChildAt(i);
1233            if (child.getVisibility() != View.GONE) {
1234                count++;
1235            }
1236        }
1237        if (mDismissView.willBeGone()) {
1238            count--;
1239        }
1240        if (mEmptyShadeView.willBeGone()) {
1241            count--;
1242        }
1243        return count;
1244    }
1245
1246    private int getMaxExpandHeight(View view) {
1247        if (view instanceof ExpandableNotificationRow) {
1248            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
1249            return row.getIntrinsicHeight();
1250        }
1251        return view.getHeight();
1252    }
1253
1254    public int getContentHeight() {
1255        return mContentHeight;
1256    }
1257
1258    private void updateContentHeight() {
1259        int height = 0;
1260        for (int i = 0; i < getChildCount(); i++) {
1261            View child = getChildAt(i);
1262            if (child.getVisibility() != View.GONE) {
1263                if (height != 0) {
1264                    // add the padding before this element
1265                    height += mPaddingBetweenElements;
1266                }
1267                if (child instanceof ExpandableNotificationRow) {
1268                    ExpandableNotificationRow row = (ExpandableNotificationRow) child;
1269                    height += row.getIntrinsicHeight();
1270                } else if (child instanceof ExpandableView) {
1271                    ExpandableView expandableView = (ExpandableView) child;
1272                    height += expandableView.getActualHeight();
1273                }
1274            }
1275        }
1276        mContentHeight = height + mTopPadding;
1277    }
1278
1279    /**
1280     * Fling the scroll view
1281     *
1282     * @param velocityY The initial velocity in the Y direction. Positive
1283     *                  numbers mean that the finger/cursor is moving down the screen,
1284     *                  which means we want to scroll towards the top.
1285     */
1286    private void fling(int velocityY) {
1287        if (getChildCount() > 0) {
1288            int scrollRange = getScrollRange();
1289
1290            float topAmount = getCurrentOverScrollAmount(true);
1291            float bottomAmount = getCurrentOverScrollAmount(false);
1292            if (velocityY < 0 && topAmount > 0) {
1293                mOwnScrollY -= (int) topAmount;
1294                mDontReportNextOverScroll = true;
1295                setOverScrollAmount(0, true, false);
1296                mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */)
1297                        * mOverflingDistance + topAmount;
1298            } else if (velocityY > 0 && bottomAmount > 0) {
1299                mOwnScrollY += bottomAmount;
1300                setOverScrollAmount(0, false, false);
1301                mMaxOverScroll = Math.abs(velocityY) / 1000f
1302                        * getRubberBandFactor(false /* onTop */) * mOverflingDistance
1303                        +  bottomAmount;
1304            } else {
1305                // it will be set once we reach the boundary
1306                mMaxOverScroll = 0.0f;
1307            }
1308            mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0,
1309                    Math.max(0, scrollRange), 0, Integer.MAX_VALUE / 2);
1310
1311            postInvalidateOnAnimation();
1312        }
1313    }
1314
1315    /**
1316     * @return Whether a fling performed on the top overscroll edge lead to the expanded
1317     * overScroll view (i.e QS).
1318     */
1319    private boolean shouldOverScrollFling(int initialVelocity) {
1320        float topOverScroll = getCurrentOverScrollAmount(true);
1321        return mScrolledToTopOnFirstDown
1322                && !mExpandedInThisMotion
1323                && topOverScroll > mMinTopOverScrollToEscape
1324                && initialVelocity > 0;
1325    }
1326
1327    public void updateTopPadding(float qsHeight, int scrollY, boolean animate) {
1328        float start = qsHeight - scrollY + mNotificationTopPadding;
1329        float stackHeight = getHeight() - start;
1330        int minStackHeight = getMinStackHeight();
1331        if (stackHeight <= minStackHeight) {
1332            float overflow = minStackHeight - stackHeight;
1333            stackHeight = minStackHeight;
1334            start = getHeight() - stackHeight;
1335            setTranslationY(overflow);
1336            mTopPaddingOverflow = overflow;
1337        } else {
1338            setTranslationY(0);
1339            mTopPaddingOverflow = 0;
1340        }
1341        setTopPadding(clampPadding((int) start), animate);
1342    }
1343
1344    public int getNotificationTopPadding() {
1345        return mNotificationTopPadding;
1346    }
1347
1348    public int getMinStackHeight() {
1349        return mCollapsedSize + mBottomStackPeekSize + mCollapseSecondCardPadding;
1350    }
1351
1352    public float getTopPaddingOverflow() {
1353        return mTopPaddingOverflow;
1354    }
1355
1356    public int getPeekHeight() {
1357        return mIntrinsicPadding + mCollapsedSize + mBottomStackPeekSize
1358                + mCollapseSecondCardPadding;
1359    }
1360
1361    private int clampPadding(int desiredPadding) {
1362        return Math.max(desiredPadding, mIntrinsicPadding);
1363    }
1364
1365    private float getRubberBandFactor(boolean onTop) {
1366        if (!onTop) {
1367            return RUBBER_BAND_FACTOR_NORMAL;
1368        }
1369        if (mExpandedInThisMotion) {
1370            return RUBBER_BAND_FACTOR_AFTER_EXPAND;
1371        } else if (mIsExpansionChanging) {
1372            return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND;
1373        } else if (mScrolledToTopOnFirstDown) {
1374            return 1.0f;
1375        }
1376        return RUBBER_BAND_FACTOR_NORMAL;
1377    }
1378
1379    /**
1380     * Accompanying function for {@link #getRubberBandFactor}: Returns true if the overscroll is
1381     * rubberbanded, false if it is technically an overscroll but rather a motion to expand the
1382     * overscroll view (e.g. expand QS).
1383     */
1384    private boolean isRubberbanded(boolean onTop) {
1385        return !onTop || mExpandedInThisMotion || mIsExpansionChanging
1386                || !mScrolledToTopOnFirstDown;
1387    }
1388
1389    private void endDrag() {
1390        setIsBeingDragged(false);
1391
1392        recycleVelocityTracker();
1393
1394        if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
1395            setOverScrollAmount(0, true /* onTop */, true /* animate */);
1396        }
1397        if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
1398            setOverScrollAmount(0, false /* onTop */, true /* animate */);
1399        }
1400    }
1401
1402    private void transformTouchEvent(MotionEvent ev, View sourceView, View targetView) {
1403        ev.offsetLocation(sourceView.getX(), sourceView.getY());
1404        ev.offsetLocation(-targetView.getX(), -targetView.getY());
1405    }
1406
1407    @Override
1408    public boolean onInterceptTouchEvent(MotionEvent ev) {
1409        if (mInterceptDelegateEnabled) {
1410            transformTouchEvent(ev, this, mScrollView);
1411            if (mScrollView.onInterceptTouchEvent(ev)) {
1412                mDelegateToScrollView = true;
1413                removeLongPressCallback();
1414                return true;
1415            }
1416            transformTouchEvent(ev, mScrollView, this);
1417        }
1418        initDownStates(ev);
1419        boolean expandWantsIt = false;
1420        if (!mSwipingInProgress && !mOnlyScrollingInThisMotion && isScrollingEnabled()) {
1421            expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev);
1422        }
1423        boolean scrollWantsIt = false;
1424        if (!mSwipingInProgress && !mExpandingNotification) {
1425            scrollWantsIt = onInterceptTouchEventScroll(ev);
1426        }
1427        boolean swipeWantsIt = false;
1428        if (!mIsBeingDragged
1429                && !mExpandingNotification
1430                && !mExpandedInThisMotion
1431                && !mOnlyScrollingInThisMotion) {
1432            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
1433        }
1434        return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev);
1435    }
1436
1437    private void initDownStates(MotionEvent ev) {
1438        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
1439            mExpandedInThisMotion = false;
1440            mOnlyScrollingInThisMotion = !mScroller.isFinished();
1441            mDisallowScrollingInThisMotion = false;
1442        }
1443    }
1444
1445    @Override
1446    protected void onViewRemoved(View child) {
1447        super.onViewRemoved(child);
1448        mStackScrollAlgorithm.notifyChildrenChanged(this);
1449        if (mChangePositionInProgress) {
1450            // This is only a position change, don't do anything special
1451            return;
1452        }
1453        ((ExpandableView) child).setOnHeightChangedListener(null);
1454        mCurrentStackScrollState.removeViewStateForView(child);
1455        updateScrollStateForRemovedChild(child);
1456        boolean animationGenerated = generateRemoveAnimation(child);
1457        if (animationGenerated && !mSwipedOutViews.contains(child)) {
1458            // Add this view to an overlay in order to ensure that it will still be temporary
1459            // drawn when removed
1460            getOverlay().add(child);
1461        }
1462
1463        // Make sure the clipRect we might have set is removed
1464        child.setClipBounds(null);
1465    }
1466
1467    /**
1468     * Generate a remove animation for a child view.
1469     *
1470     * @param child The view to generate the remove animation for.
1471     * @return Whether an animation was generated.
1472     */
1473    private boolean generateRemoveAnimation(View child) {
1474        if (mIsExpanded && mAnimationsEnabled) {
1475            if (!mChildrenToAddAnimated.contains(child)) {
1476                // Generate Animations
1477                mChildrenToRemoveAnimated.add(child);
1478                mNeedsAnimation = true;
1479                return true;
1480            } else {
1481                mChildrenToAddAnimated.remove(child);
1482                mFromMoreCardAdditions.remove(child);
1483                return false;
1484            }
1485        }
1486        return false;
1487    }
1488
1489    /**
1490     * Updates the scroll position when a child was removed
1491     *
1492     * @param removedChild the removed child
1493     */
1494    private void updateScrollStateForRemovedChild(View removedChild) {
1495        int startingPosition = getPositionInLinearLayout(removedChild);
1496        int childHeight = getIntrinsicHeight(removedChild) + mPaddingBetweenElements;
1497        int endPosition = startingPosition + childHeight;
1498        if (endPosition <= mOwnScrollY) {
1499            // This child is fully scrolled of the top, so we have to deduct its height from the
1500            // scrollPosition
1501            mOwnScrollY -= childHeight;
1502        } else if (startingPosition < mOwnScrollY) {
1503            // This child is currently being scrolled into, set the scroll position to the start of
1504            // this child
1505            mOwnScrollY = startingPosition;
1506        }
1507    }
1508
1509    private int getIntrinsicHeight(View view) {
1510        if (view instanceof ExpandableView) {
1511            ExpandableView expandableView = (ExpandableView) view;
1512            return expandableView.getIntrinsicHeight();
1513        }
1514        return view.getHeight();
1515    }
1516
1517    private int getPositionInLinearLayout(View requestedChild) {
1518        int position = 0;
1519        for (int i = 0; i < getChildCount(); i++) {
1520            View child = getChildAt(i);
1521            if (child == requestedChild) {
1522                return position;
1523            }
1524            if (child.getVisibility() != View.GONE) {
1525                position += child.getHeight();
1526                if (i < getChildCount()-1) {
1527                    position += mPaddingBetweenElements;
1528                }
1529            }
1530        }
1531        return 0;
1532    }
1533
1534    @Override
1535    protected void onViewAdded(View child) {
1536        super.onViewAdded(child);
1537        mStackScrollAlgorithm.notifyChildrenChanged(this);
1538        ((ExpandableView) child).setOnHeightChangedListener(this);
1539        generateAddAnimation(child, false /* fromMoreCard */);
1540    }
1541
1542    public void setAnimationsEnabled(boolean animationsEnabled) {
1543        mAnimationsEnabled = animationsEnabled;
1544    }
1545
1546    public boolean isAddOrRemoveAnimationPending() {
1547        return mNeedsAnimation
1548                && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
1549    }
1550    /**
1551     * Generate an animation for an added child view.
1552     *
1553     * @param child The view to be added.
1554     * @param fromMoreCard Whether this add is coming from the "more" card on lockscreen.
1555     */
1556    public void generateAddAnimation(View child, boolean fromMoreCard) {
1557        if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) {
1558            // Generate Animations
1559            mChildrenToAddAnimated.add(child);
1560            if (fromMoreCard) {
1561                mFromMoreCardAdditions.add(child);
1562            }
1563            mNeedsAnimation = true;
1564        }
1565    }
1566
1567    /**
1568     * Change the position of child to a new location
1569     *
1570     * @param child the view to change the position for
1571     * @param newIndex the new index
1572     */
1573    public void changeViewPosition(View child, int newIndex) {
1574        int currentIndex = indexOfChild(child);
1575        if (child != null && child.getParent() == this && currentIndex != newIndex) {
1576            mChangePositionInProgress = true;
1577            removeView(child);
1578            addView(child, newIndex);
1579            mChangePositionInProgress = false;
1580            if (mIsExpanded && mAnimationsEnabled && child.getVisibility() != View.GONE) {
1581                mChildrenChangingPositions.add(child);
1582                mNeedsAnimation = true;
1583            }
1584        }
1585    }
1586
1587    private void startAnimationToState() {
1588        if (mNeedsAnimation) {
1589            generateChildHierarchyEvents();
1590            mNeedsAnimation = false;
1591        }
1592        if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
1593            mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState,
1594                    mGoToFullShadeDelay);
1595            mAnimationEvents.clear();
1596        } else {
1597            applyCurrentState();
1598        }
1599        mGoToFullShadeDelay = 0;
1600    }
1601
1602    private void generateChildHierarchyEvents() {
1603        generateChildRemovalEvents();
1604        generateChildAdditionEvents();
1605        generatePositionChangeEvents();
1606        generateSnapBackEvents();
1607        generateDragEvents();
1608        generateTopPaddingEvent();
1609        generateActivateEvent();
1610        generateDimmedEvent();
1611        generateHideSensitiveEvent();
1612        generateDarkEvent();
1613        generateGoToFullShadeEvent();
1614        generateViewResizeEvent();
1615        mNeedsAnimation = false;
1616    }
1617
1618    private void generateViewResizeEvent() {
1619        if (mNeedViewResizeAnimation) {
1620            mAnimationEvents.add(
1621                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_VIEW_RESIZE));
1622        }
1623        mNeedViewResizeAnimation = false;
1624    }
1625
1626    private void generateSnapBackEvents() {
1627        for (View child : mSnappedBackChildren) {
1628            mAnimationEvents.add(new AnimationEvent(child,
1629                    AnimationEvent.ANIMATION_TYPE_SNAP_BACK));
1630        }
1631        mSnappedBackChildren.clear();
1632    }
1633
1634    private void generateDragEvents() {
1635        for (View child : mDragAnimPendingChildren) {
1636            mAnimationEvents.add(new AnimationEvent(child,
1637                    AnimationEvent.ANIMATION_TYPE_START_DRAG));
1638        }
1639        mDragAnimPendingChildren.clear();
1640    }
1641
1642    private void generateChildRemovalEvents() {
1643        for (View child : mChildrenToRemoveAnimated) {
1644            boolean childWasSwipedOut = mSwipedOutViews.contains(child);
1645            int animationType = childWasSwipedOut
1646                    ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
1647                    : AnimationEvent.ANIMATION_TYPE_REMOVE;
1648            AnimationEvent event = new AnimationEvent(child, animationType);
1649
1650            // we need to know the view after this one
1651            event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY());
1652            mAnimationEvents.add(event);
1653        }
1654        mSwipedOutViews.clear();
1655        mChildrenToRemoveAnimated.clear();
1656    }
1657
1658    private void generatePositionChangeEvents() {
1659        for (View child : mChildrenChangingPositions) {
1660            mAnimationEvents.add(new AnimationEvent(child,
1661                    AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
1662        }
1663        mChildrenChangingPositions.clear();
1664    }
1665
1666    private void generateChildAdditionEvents() {
1667        for (View child : mChildrenToAddAnimated) {
1668            if (mFromMoreCardAdditions.contains(child)) {
1669                mAnimationEvents.add(new AnimationEvent(child,
1670                        AnimationEvent.ANIMATION_TYPE_ADD,
1671                        StackStateAnimator.ANIMATION_DURATION_STANDARD));
1672            } else {
1673                mAnimationEvents.add(new AnimationEvent(child,
1674                        AnimationEvent.ANIMATION_TYPE_ADD));
1675            }
1676        }
1677        mChildrenToAddAnimated.clear();
1678        mFromMoreCardAdditions.clear();
1679    }
1680
1681    private void generateTopPaddingEvent() {
1682        if (mTopPaddingNeedsAnimation) {
1683            mAnimationEvents.add(
1684                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED));
1685        }
1686        mTopPaddingNeedsAnimation = false;
1687    }
1688
1689    private void generateActivateEvent() {
1690        if (mActivateNeedsAnimation) {
1691            mAnimationEvents.add(
1692                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD));
1693        }
1694        mActivateNeedsAnimation = false;
1695    }
1696
1697    private void generateDimmedEvent() {
1698        if (mDimmedNeedsAnimation) {
1699            mAnimationEvents.add(
1700                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED));
1701        }
1702        mDimmedNeedsAnimation = false;
1703    }
1704
1705    private void generateHideSensitiveEvent() {
1706        if (mHideSensitiveNeedsAnimation) {
1707            mAnimationEvents.add(
1708                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_HIDE_SENSITIVE));
1709        }
1710        mHideSensitiveNeedsAnimation = false;
1711    }
1712
1713    private void generateDarkEvent() {
1714        if (mDarkNeedsAnimation) {
1715            mAnimationEvents.add(
1716                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DARK));
1717        }
1718        mDarkNeedsAnimation = false;
1719    }
1720
1721    private void generateGoToFullShadeEvent() {
1722        if (mGoToFullShadeNeedsAnimation) {
1723            mAnimationEvents.add(
1724                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE));
1725        }
1726        mGoToFullShadeNeedsAnimation = false;
1727    }
1728
1729    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
1730        if (!isScrollingEnabled()) {
1731            return false;
1732        }
1733        /*
1734         * This method JUST determines whether we want to intercept the motion.
1735         * If we return true, onMotionEvent will be called and we do the actual
1736         * scrolling there.
1737         */
1738
1739        /*
1740        * Shortcut the most recurring case: the user is in the dragging
1741        * state and he is moving his finger.  We want to intercept this
1742        * motion.
1743        */
1744        final int action = ev.getAction();
1745        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
1746            return true;
1747        }
1748
1749        switch (action & MotionEvent.ACTION_MASK) {
1750            case MotionEvent.ACTION_MOVE: {
1751                /*
1752                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
1753                 * whether the user has moved far enough from his original down touch.
1754                 */
1755
1756                /*
1757                * Locally do absolute value. mLastMotionY is set to the y value
1758                * of the down event.
1759                */
1760                final int activePointerId = mActivePointerId;
1761                if (activePointerId == INVALID_POINTER) {
1762                    // If we don't have a valid id, the touch down wasn't on content.
1763                    break;
1764                }
1765
1766                final int pointerIndex = ev.findPointerIndex(activePointerId);
1767                if (pointerIndex == -1) {
1768                    Log.e(TAG, "Invalid pointerId=" + activePointerId
1769                            + " in onInterceptTouchEvent");
1770                    break;
1771                }
1772
1773                final int y = (int) ev.getY(pointerIndex);
1774                final int x = (int) ev.getX(pointerIndex);
1775                final int yDiff = Math.abs(y - mLastMotionY);
1776                final int xDiff = Math.abs(x - mDownX);
1777                if (yDiff > mTouchSlop && yDiff > xDiff) {
1778                    setIsBeingDragged(true);
1779                    mLastMotionY = y;
1780                    mDownX = x;
1781                    initVelocityTrackerIfNotExists();
1782                    mVelocityTracker.addMovement(ev);
1783                }
1784                break;
1785            }
1786
1787            case MotionEvent.ACTION_DOWN: {
1788                final int y = (int) ev.getY();
1789                if (getChildAtPosition(ev.getX(), y) == null) {
1790                    setIsBeingDragged(false);
1791                    recycleVelocityTracker();
1792                    break;
1793                }
1794
1795                /*
1796                 * Remember location of down touch.
1797                 * ACTION_DOWN always refers to pointer index 0.
1798                 */
1799                mLastMotionY = y;
1800                mDownX = (int) ev.getX();
1801                mActivePointerId = ev.getPointerId(0);
1802                mScrolledToTopOnFirstDown = isScrolledToTop();
1803
1804                initOrResetVelocityTracker();
1805                mVelocityTracker.addMovement(ev);
1806                /*
1807                * If being flinged and user touches the screen, initiate drag;
1808                * otherwise don't.  mScroller.isFinished should be false when
1809                * being flinged.
1810                */
1811                boolean isBeingDragged = !mScroller.isFinished();
1812                setIsBeingDragged(isBeingDragged);
1813                break;
1814            }
1815
1816            case MotionEvent.ACTION_CANCEL:
1817            case MotionEvent.ACTION_UP:
1818                /* Release the drag */
1819                setIsBeingDragged(false);
1820                mActivePointerId = INVALID_POINTER;
1821                recycleVelocityTracker();
1822                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
1823                    postInvalidateOnAnimation();
1824                }
1825                break;
1826            case MotionEvent.ACTION_POINTER_UP:
1827                onSecondaryPointerUp(ev);
1828                break;
1829        }
1830
1831        /*
1832        * The only time we want to intercept motion events is if we are in the
1833        * drag mode.
1834        */
1835        return mIsBeingDragged;
1836    }
1837
1838    /**
1839     * @return Whether the specified motion event is actually happening over the content.
1840     */
1841    private boolean isInContentBounds(MotionEvent event) {
1842        return event.getY() < getHeight() - getEmptyBottomMargin();
1843    }
1844
1845    private void setIsBeingDragged(boolean isDragged) {
1846        mIsBeingDragged = isDragged;
1847        if (isDragged) {
1848            requestDisallowInterceptTouchEvent(true);
1849            removeLongPressCallback();
1850        }
1851    }
1852
1853    @Override
1854    public void onWindowFocusChanged(boolean hasWindowFocus) {
1855        super.onWindowFocusChanged(hasWindowFocus);
1856        if (!hasWindowFocus) {
1857            removeLongPressCallback();
1858        }
1859    }
1860
1861    public void removeLongPressCallback() {
1862        mSwipeHelper.removeLongPressCallback();
1863    }
1864
1865    @Override
1866    public boolean isScrolledToTop() {
1867        return mOwnScrollY == 0;
1868    }
1869
1870    @Override
1871    public boolean isScrolledToBottom() {
1872        return mOwnScrollY >= getScrollRange();
1873    }
1874
1875    @Override
1876    public View getHostView() {
1877        return this;
1878    }
1879
1880    public int getEmptyBottomMargin() {
1881        int emptyMargin = mMaxLayoutHeight - mContentHeight - mBottomStackPeekSize;
1882        if (needsHeightAdaption()) {
1883            emptyMargin -= mBottomStackSlowDownHeight;
1884        } else {
1885            emptyMargin -= mCollapseSecondCardPadding;
1886        }
1887        return Math.max(emptyMargin, 0);
1888    }
1889
1890    public void onExpansionStarted() {
1891        mIsExpansionChanging = true;
1892        mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState);
1893    }
1894
1895    public void onExpansionStopped() {
1896        mIsExpansionChanging = false;
1897        mStackScrollAlgorithm.onExpansionStopped();
1898        if (!mIsExpanded) {
1899            mOwnScrollY = 0;
1900        }
1901    }
1902
1903    private void setIsExpanded(boolean isExpanded) {
1904        mIsExpanded = isExpanded;
1905        mStackScrollAlgorithm.setIsExpanded(isExpanded);
1906    }
1907
1908    @Override
1909    public void onHeightChanged(ExpandableView view) {
1910        updateContentHeight();
1911        updateScrollPositionOnExpandInBottom(view);
1912        clampScrollPosition();
1913        notifyHeightChangeListener(view);
1914        requestChildrenUpdate();
1915    }
1916
1917    @Override
1918    public void onReset(ExpandableView view) {
1919        if (mIsExpanded && mAnimationsEnabled) {
1920            mRequestViewResizeAnimationOnLayout = true;
1921        }
1922        mStackScrollAlgorithm.onReset(view);
1923    }
1924
1925    private void updateScrollPositionOnExpandInBottom(ExpandableView view) {
1926        if (view instanceof ExpandableNotificationRow) {
1927            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
1928            if (row.isUserLocked()) {
1929                // We are actually expanding this view
1930                float endPosition = row.getTranslationY() + row.getActualHeight();
1931                int stackEnd = mMaxLayoutHeight - mBottomStackPeekSize -
1932                        mBottomStackSlowDownHeight;
1933                if (endPosition > stackEnd) {
1934                    mOwnScrollY += endPosition - stackEnd;
1935                    mDisallowScrollingInThisMotion = true;
1936                }
1937            }
1938        }
1939    }
1940
1941    public void setOnHeightChangedListener(
1942            ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
1943        this.mOnHeightChangedListener = mOnHeightChangedListener;
1944    }
1945
1946    public void onChildAnimationFinished() {
1947        requestChildrenUpdate();
1948    }
1949
1950    /**
1951     * See {@link AmbientState#setDimmed}.
1952     */
1953    public void setDimmed(boolean dimmed, boolean animate) {
1954        mStackScrollAlgorithm.setDimmed(dimmed);
1955        mAmbientState.setDimmed(dimmed);
1956        updatePadding(dimmed);
1957        if (animate && mAnimationsEnabled) {
1958            mDimmedNeedsAnimation = true;
1959            mNeedsAnimation =  true;
1960        }
1961        requestChildrenUpdate();
1962    }
1963
1964    public void setHideSensitive(boolean hideSensitive, boolean animate) {
1965        if (hideSensitive != mAmbientState.isHideSensitive()) {
1966            int childCount = getChildCount();
1967            for (int i = 0; i < childCount; i++) {
1968                ExpandableView v = (ExpandableView) getChildAt(i);
1969                v.setHideSensitiveForIntrinsicHeight(hideSensitive);
1970            }
1971            mAmbientState.setHideSensitive(hideSensitive);
1972            if (animate && mAnimationsEnabled) {
1973                mHideSensitiveNeedsAnimation = true;
1974                mNeedsAnimation =  true;
1975            }
1976            requestChildrenUpdate();
1977        }
1978    }
1979
1980    /**
1981     * See {@link AmbientState#setActivatedChild}.
1982     */
1983    public void setActivatedChild(ActivatableNotificationView activatedChild) {
1984        mAmbientState.setActivatedChild(activatedChild);
1985        if (mAnimationsEnabled) {
1986            mActivateNeedsAnimation = true;
1987            mNeedsAnimation =  true;
1988        }
1989        requestChildrenUpdate();
1990    }
1991
1992    public ActivatableNotificationView getActivatedChild() {
1993        return mAmbientState.getActivatedChild();
1994    }
1995
1996    private void applyCurrentState() {
1997        mCurrentStackScrollState.apply();
1998        if (mListener != null) {
1999            mListener.onChildLocationsChanged(this);
2000        }
2001    }
2002
2003    public void setSpeedBumpView(SpeedBumpView speedBumpView) {
2004        mSpeedBumpView = speedBumpView;
2005        addView(speedBumpView);
2006    }
2007
2008    private void updateSpeedBump(boolean visible) {
2009        boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE;
2010        if (visible != notGoneBefore) {
2011            int newVisibility = visible ? VISIBLE : GONE;
2012            mSpeedBumpView.setVisibility(newVisibility);
2013            if (visible) {
2014                // Make invisible to ensure that the appear animation is played.
2015                mSpeedBumpView.setInvisible();
2016            } else {
2017                // TODO: This doesn't really work, because the view is already set to GONE above.
2018                generateRemoveAnimation(mSpeedBumpView);
2019            }
2020        }
2021    }
2022
2023    public void goToFullShade(long delay) {
2024        updateSpeedBump(true /* visibility */);
2025        mDismissView.setInvisible();
2026        mEmptyShadeView.setInvisible();
2027        mGoToFullShadeNeedsAnimation = true;
2028        mGoToFullShadeDelay = delay;
2029        mNeedsAnimation =  true;
2030        requestChildrenUpdate();
2031    }
2032
2033    public void cancelExpandHelper() {
2034        mExpandHelper.cancel();
2035    }
2036
2037    public void setIntrinsicPadding(int intrinsicPadding) {
2038        mIntrinsicPadding = intrinsicPadding;
2039    }
2040
2041    public int getIntrinsicPadding() {
2042        return mIntrinsicPadding;
2043    }
2044
2045    /**
2046     * @return the y position of the first notification
2047     */
2048    public float getNotificationsTopY() {
2049        return mTopPadding + getTranslationY();
2050    }
2051
2052    @Override
2053    public boolean shouldDelayChildPressedState() {
2054        return true;
2055    }
2056
2057    public void setScrimAlpha(float progress) {
2058        if (progress != mAmbientState.getScrimAmount()) {
2059            mAmbientState.setScrimAmount(progress);
2060            requestChildrenUpdate();
2061        }
2062    }
2063
2064    /**
2065     * See {@link AmbientState#setDark}.
2066     */
2067    public void setDark(boolean dark, boolean animate) {
2068        mAmbientState.setDark(dark);
2069        if (animate && mAnimationsEnabled) {
2070            mDarkNeedsAnimation = true;
2071            mNeedsAnimation =  true;
2072        }
2073        requestChildrenUpdate();
2074    }
2075
2076    public void setDismissView(DismissView dismissView) {
2077        mDismissView = dismissView;
2078        addView(mDismissView);
2079    }
2080
2081    public void setEmptyShadeView(EmptyShadeView emptyShadeView) {
2082        mEmptyShadeView = emptyShadeView;
2083        addView(mEmptyShadeView);
2084    }
2085
2086    public void updateEmptyShadeView(boolean visible) {
2087        int oldVisibility = mEmptyShadeView.willBeGone() ? GONE : mEmptyShadeView.getVisibility();
2088        int newVisibility = visible ? VISIBLE : GONE;
2089        if (oldVisibility != newVisibility) {
2090            if (oldVisibility == GONE) {
2091                if (mEmptyShadeView.willBeGone()) {
2092                    mEmptyShadeView.cancelAnimation();
2093                } else {
2094                    mEmptyShadeView.setInvisible();
2095                    mEmptyShadeView.setVisibility(newVisibility);
2096                }
2097                mEmptyShadeView.setWillBeGone(false);
2098                updateContentHeight();
2099            } else {
2100                mEmptyShadeView.setWillBeGone(true);
2101                mEmptyShadeView.performVisibilityAnimation(false, new Runnable() {
2102                    @Override
2103                    public void run() {
2104                        mEmptyShadeView.setVisibility(GONE);
2105                        mEmptyShadeView.setWillBeGone(false);
2106                        updateContentHeight();
2107                    }
2108                });
2109            }
2110        }
2111    }
2112
2113    public void updateDismissView(boolean visible) {
2114        int oldVisibility = mDismissView.willBeGone() ? GONE : mDismissView.getVisibility();
2115        int newVisibility = visible ? VISIBLE : GONE;
2116        if (oldVisibility != newVisibility) {
2117            if (oldVisibility == GONE) {
2118                if (mDismissView.willBeGone()) {
2119                    mDismissView.cancelAnimation();
2120                } else {
2121                    mDismissView.setInvisible();
2122                    mDismissView.setVisibility(newVisibility);
2123                }
2124                mDismissView.setWillBeGone(false);
2125                updateContentHeight();
2126            } else {
2127                mDismissView.setWillBeGone(true);
2128                mDismissView.performVisibilityAnimation(false, new Runnable() {
2129                    @Override
2130                    public void run() {
2131                        mDismissView.setVisibility(GONE);
2132                        mDismissView.setWillBeGone(false);
2133                        updateContentHeight();
2134                    }
2135                });
2136            }
2137        }
2138    }
2139
2140    public void setDismissAllInProgress(boolean dismissAllInProgress) {
2141        mDismissAllInProgress = dismissAllInProgress;
2142    }
2143
2144    public boolean isDismissViewNotGone() {
2145        return mDismissView.getVisibility() != View.GONE && !mDismissView.willBeGone();
2146    }
2147
2148    public boolean isDismissViewVisible() {
2149        return mDismissView.isVisible();
2150    }
2151
2152    public int getDismissViewHeight() {
2153        int height = mDismissView.getHeight() + mPaddingBetweenElementsNormal;
2154
2155        // Hack: Accommodate for additional distance when we only have one notification and the
2156        // dismiss all button.
2157        if (getNotGoneChildCount() == 2 && getLastChildNotGone() == mDismissView
2158                && getFirstChildNotGone() instanceof ActivatableNotificationView) {
2159            height += mCollapseSecondCardPadding;
2160        }
2161        return height;
2162    }
2163
2164    public float getBottomMostNotificationBottom() {
2165        final int count = getChildCount();
2166        float max = 0;
2167        for (int childIdx = 0; childIdx < count; childIdx++) {
2168            ExpandableView child = (ExpandableView) getChildAt(childIdx);
2169            if (child.getVisibility() == GONE) {
2170                continue;
2171            }
2172            float bottom = child.getTranslationY() + child.getActualHeight();
2173            if (bottom > max) {
2174                max = bottom;
2175            }
2176        }
2177        return max + getTranslationY();
2178    }
2179
2180    /**
2181     * @param qsMinHeight The minimum height of the quick settings including padding
2182     *                    See {@link StackScrollAlgorithm#updateIsSmallScreen}.
2183     */
2184    public void updateIsSmallScreen(int qsMinHeight) {
2185        mStackScrollAlgorithm.updateIsSmallScreen(mMaxLayoutHeight - qsMinHeight);
2186    }
2187
2188    /**
2189     * A listener that is notified when some child locations might have changed.
2190     */
2191    public interface OnChildLocationsChangedListener {
2192        public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
2193    }
2194
2195    /**
2196     * A listener that gets notified when the overscroll at the top has changed.
2197     */
2198    public interface OnOverscrollTopChangedListener {
2199
2200        /**
2201         * Notifies a listener that the overscroll has changed.
2202         *
2203         * @param amount the amount of overscroll, in pixels
2204         * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an
2205         *                     unrubberbanded motion to directly expand overscroll view (e.g expand
2206         *                     QS)
2207         */
2208        public void onOverscrollTopChanged(float amount, boolean isRubberbanded);
2209
2210        /**
2211         * Notify a listener that the scroller wants to escape from the scrolling motion and
2212         * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
2213         *
2214         * @param velocity The velocity that the Scroller had when over flinging
2215         * @param open Should the fling open or close the overscroll view.
2216         */
2217        public void flingTopOverscroll(float velocity, boolean open);
2218    }
2219
2220    static class AnimationEvent {
2221
2222        static AnimationFilter[] FILTERS = new AnimationFilter[] {
2223
2224                // ANIMATION_TYPE_ADD
2225                new AnimationFilter()
2226                        .animateAlpha()
2227                        .animateHeight()
2228                        .animateTopInset()
2229                        .animateY()
2230                        .animateZ()
2231                        .hasDelays(),
2232
2233                // ANIMATION_TYPE_REMOVE
2234                new AnimationFilter()
2235                        .animateAlpha()
2236                        .animateHeight()
2237                        .animateTopInset()
2238                        .animateY()
2239                        .animateZ()
2240                        .hasDelays(),
2241
2242                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
2243                new AnimationFilter()
2244                        .animateAlpha()
2245                        .animateHeight()
2246                        .animateTopInset()
2247                        .animateY()
2248                        .animateZ()
2249                        .hasDelays(),
2250
2251                // ANIMATION_TYPE_TOP_PADDING_CHANGED
2252                new AnimationFilter()
2253                        .animateAlpha()
2254                        .animateHeight()
2255                        .animateTopInset()
2256                        .animateY()
2257                        .animateDimmed()
2258                        .animateScale()
2259                        .animateZ(),
2260
2261                // ANIMATION_TYPE_START_DRAG
2262                new AnimationFilter()
2263                        .animateAlpha(),
2264
2265                // ANIMATION_TYPE_SNAP_BACK
2266                new AnimationFilter()
2267                        .animateAlpha()
2268                        .animateHeight(),
2269
2270                // ANIMATION_TYPE_ACTIVATED_CHILD
2271                new AnimationFilter()
2272                        .animateScale()
2273                        .animateAlpha(),
2274
2275                // ANIMATION_TYPE_DIMMED
2276                new AnimationFilter()
2277                        .animateY()
2278                        .animateScale()
2279                        .animateDimmed(),
2280
2281                // ANIMATION_TYPE_CHANGE_POSITION
2282                new AnimationFilter()
2283                        .animateAlpha()
2284                        .animateHeight()
2285                        .animateTopInset()
2286                        .animateY()
2287                        .animateZ(),
2288
2289                // ANIMATION_TYPE_DARK
2290                new AnimationFilter()
2291                        .animateDark(),
2292
2293                // ANIMATION_TYPE_GO_TO_FULL_SHADE
2294                new AnimationFilter()
2295                        .animateAlpha()
2296                        .animateHeight()
2297                        .animateTopInset()
2298                        .animateY()
2299                        .animateDimmed()
2300                        .animateScale()
2301                        .animateZ()
2302                        .hasDelays(),
2303
2304                // ANIMATION_TYPE_HIDE_SENSITIVE
2305                new AnimationFilter()
2306                        .animateHideSensitive(),
2307
2308                // ANIMATION_TYPE_VIEW_RESIZE
2309                new AnimationFilter()
2310                        .animateAlpha()
2311                        .animateHeight()
2312                        .animateTopInset()
2313                        .animateY()
2314                        .animateZ(),
2315        };
2316
2317        static int[] LENGTHS = new int[] {
2318
2319                // ANIMATION_TYPE_ADD
2320                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
2321
2322                // ANIMATION_TYPE_REMOVE
2323                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
2324
2325                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
2326                StackStateAnimator.ANIMATION_DURATION_STANDARD,
2327
2328                // ANIMATION_TYPE_TOP_PADDING_CHANGED
2329                StackStateAnimator.ANIMATION_DURATION_STANDARD,
2330
2331                // ANIMATION_TYPE_START_DRAG
2332                StackStateAnimator.ANIMATION_DURATION_STANDARD,
2333
2334                // ANIMATION_TYPE_SNAP_BACK
2335                StackStateAnimator.ANIMATION_DURATION_STANDARD,
2336
2337                // ANIMATION_TYPE_ACTIVATED_CHILD
2338                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
2339
2340                // ANIMATION_TYPE_DIMMED
2341                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
2342
2343                // ANIMATION_TYPE_CHANGE_POSITION
2344                StackStateAnimator.ANIMATION_DURATION_STANDARD,
2345
2346                // ANIMATION_TYPE_DARK
2347                StackStateAnimator.ANIMATION_DURATION_STANDARD,
2348
2349                // ANIMATION_TYPE_GO_TO_FULL_SHADE
2350                StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE,
2351
2352                // ANIMATION_TYPE_HIDE_SENSITIVE
2353                StackStateAnimator.ANIMATION_DURATION_STANDARD,
2354
2355                // ANIMATION_TYPE_VIEW_RESIZE
2356                StackStateAnimator.ANIMATION_DURATION_STANDARD,
2357        };
2358
2359        static final int ANIMATION_TYPE_ADD = 0;
2360        static final int ANIMATION_TYPE_REMOVE = 1;
2361        static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
2362        static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
2363        static final int ANIMATION_TYPE_START_DRAG = 4;
2364        static final int ANIMATION_TYPE_SNAP_BACK = 5;
2365        static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
2366        static final int ANIMATION_TYPE_DIMMED = 7;
2367        static final int ANIMATION_TYPE_CHANGE_POSITION = 8;
2368        static final int ANIMATION_TYPE_DARK = 9;
2369        static final int ANIMATION_TYPE_GO_TO_FULL_SHADE = 10;
2370        static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11;
2371        static final int ANIMATION_TYPE_VIEW_RESIZE = 12;
2372
2373        final long eventStartTime;
2374        final View changingView;
2375        final int animationType;
2376        final AnimationFilter filter;
2377        final long length;
2378        View viewAfterChangingView;
2379
2380        AnimationEvent(View view, int type) {
2381            this(view, type, LENGTHS[type]);
2382        }
2383
2384        AnimationEvent(View view, int type, long length) {
2385            eventStartTime = AnimationUtils.currentAnimationTimeMillis();
2386            changingView = view;
2387            animationType = type;
2388            filter = FILTERS[type];
2389            this.length = length;
2390        }
2391
2392        /**
2393         * Combines the length of several animation events into a single value.
2394         *
2395         * @param events The events of the lengths to combine.
2396         * @return The combined length. Depending on the event types, this might be the maximum of
2397         *         all events or the length of a specific event.
2398         */
2399        static long combineLength(ArrayList<AnimationEvent> events) {
2400            long length = 0;
2401            int size = events.size();
2402            for (int i = 0; i < size; i++) {
2403                AnimationEvent event = events.get(i);
2404                length = Math.max(length, event.length);
2405                if (event.animationType == ANIMATION_TYPE_GO_TO_FULL_SHADE) {
2406                    return event.length;
2407                }
2408            }
2409            return length;
2410        }
2411    }
2412
2413}
2414