NotificationStackScrollLayout.java revision 457cc356089c61317b4c29a3e83f5fc47edb68be
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.ExpandableNotificationRow;
38import com.android.systemui.statusbar.ExpandableView;
39import com.android.systemui.statusbar.SpeedBumpView;
40import com.android.systemui.statusbar.policy.ScrollAdapter;
41import com.android.systemui.statusbar.stack.StackScrollState.ViewState;
42
43import java.util.ArrayList;
44
45/**
46 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
47 */
48public class NotificationStackScrollLayout extends ViewGroup
49        implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
50        ExpandableView.OnHeightChangedListener {
51
52    private static final String TAG = "NotificationStackScrollLayout";
53    private static final boolean DEBUG = false;
54    private static final float RUBBER_BAND_FACTOR = 0.35f;
55
56    /**
57     * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
58     */
59    private static final int INVALID_POINTER = -1;
60
61    private SwipeHelper mSwipeHelper;
62    private boolean mSwipingInProgress;
63    private int mCurrentStackHeight = Integer.MAX_VALUE;
64    private int mOwnScrollY;
65    private int mMaxLayoutHeight;
66
67    private VelocityTracker mVelocityTracker;
68    private OverScroller mScroller;
69    private int mTouchSlop;
70    private int mMinimumVelocity;
71    private int mMaximumVelocity;
72    private int mOverflingDistance;
73    private float mMaxOverScroll;
74    private boolean mIsBeingDragged;
75    private int mLastMotionY;
76    private int mActivePointerId;
77
78    private int mSidePaddings;
79    private Paint mDebugPaint;
80    private int mContentHeight;
81    private int mCollapsedSize;
82    private int mBottomStackSlowDownHeight;
83    private int mBottomStackPeekSize;
84    private int mPaddingBetweenElements;
85    private int mPaddingBetweenElementsDimmed;
86    private int mPaddingBetweenElementsNormal;
87    private int mTopPadding;
88
89    /**
90     * The algorithm which calculates the properties for our children
91     */
92    private StackScrollAlgorithm mStackScrollAlgorithm;
93
94    /**
95     * The current State this Layout is in
96     */
97    private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
98    private AmbientState mAmbientState = new AmbientState();
99    private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>();
100    private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
101    private ArrayList<View> mSnappedBackChildren = new ArrayList<View>();
102    private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>();
103    private ArrayList<View> mChildrenChangingPositions = new ArrayList<View>();
104    private ArrayList<AnimationEvent> mAnimationEvents
105            = new ArrayList<AnimationEvent>();
106    private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
107    private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
108    private boolean mAnimationsEnabled;
109
110    /**
111     * The raw amount of the overScroll on the top, which is not rubber-banded.
112     */
113    private float mOverScrolledTopPixels;
114
115    /**
116     * The raw amount of the overScroll on the bottom, which is not rubber-banded.
117     */
118    private float mOverScrolledBottomPixels;
119
120    private OnChildLocationsChangedListener mListener;
121    private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
122    private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
123    private boolean mNeedsAnimation;
124    private boolean mTopPaddingNeedsAnimation;
125    private boolean mDimmedNeedsAnimation;
126    private boolean mActivateNeedsAnimation;
127    private boolean mIsExpanded = true;
128    private boolean mChildrenUpdateRequested;
129    private SpeedBumpView mSpeedBumpView;
130    private boolean mIsExpansionChanging;
131    private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
132            = new ViewTreeObserver.OnPreDrawListener() {
133        @Override
134        public boolean onPreDraw() {
135            updateChildren();
136            mChildrenUpdateRequested = false;
137            getViewTreeObserver().removeOnPreDrawListener(this);
138            return true;
139        }
140    };
141
142    public NotificationStackScrollLayout(Context context) {
143        this(context, null);
144    }
145
146    public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
147        this(context, attrs, 0);
148    }
149
150    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
151        this(context, attrs, defStyleAttr, 0);
152    }
153
154    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
155            int defStyleRes) {
156        super(context, attrs, defStyleAttr, defStyleRes);
157        initView(context);
158        if (DEBUG) {
159            setWillNotDraw(false);
160            mDebugPaint = new Paint();
161            mDebugPaint.setColor(0xffff0000);
162            mDebugPaint.setStrokeWidth(2);
163            mDebugPaint.setStyle(Paint.Style.STROKE);
164        }
165    }
166
167    @Override
168    protected void onDraw(Canvas canvas) {
169        if (DEBUG) {
170            int y = mCollapsedSize;
171            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
172            y = (int) (getLayoutHeight() - mBottomStackPeekSize
173                    - mBottomStackSlowDownHeight);
174            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
175            y = (int) (getLayoutHeight() - mBottomStackPeekSize);
176            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
177            y = (int) getLayoutHeight();
178            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
179            y = getHeight() - getEmptyBottomMargin();
180            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
181        }
182    }
183
184    private void initView(Context context) {
185        mScroller = new OverScroller(getContext());
186        setFocusable(true);
187        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
188        setClipChildren(false);
189        final ViewConfiguration configuration = ViewConfiguration.get(context);
190        mTouchSlop = configuration.getScaledTouchSlop();
191        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
192        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
193        mOverflingDistance = configuration.getScaledOverflingDistance();
194        float densityScale = getResources().getDisplayMetrics().density;
195        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
196        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
197
198        mSidePaddings = context.getResources()
199                .getDimensionPixelSize(R.dimen.notification_side_padding);
200        mCollapsedSize = context.getResources()
201                .getDimensionPixelSize(R.dimen.notification_min_height);
202        mBottomStackPeekSize = context.getResources()
203                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
204        mStackScrollAlgorithm = new StackScrollAlgorithm(context);
205        mPaddingBetweenElementsDimmed = context.getResources()
206                .getDimensionPixelSize(R.dimen.notification_padding_dimmed);
207        mPaddingBetweenElementsNormal = context.getResources()
208                .getDimensionPixelSize(R.dimen.notification_padding);
209        updatePadding(false);
210    }
211
212    private void updatePadding(boolean dimmed) {
213        mPaddingBetweenElements = dimmed
214                ? mPaddingBetweenElementsDimmed
215                : mPaddingBetweenElementsNormal;
216        mBottomStackSlowDownHeight = mStackScrollAlgorithm.getBottomStackSlowDownLength();
217        updateContentHeight();
218    }
219
220    @Override
221    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
222        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
223        int mode = MeasureSpec.getMode(widthMeasureSpec);
224        int size = MeasureSpec.getSize(widthMeasureSpec);
225        int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode);
226        measureChildren(childMeasureSpec, heightMeasureSpec);
227    }
228
229    @Override
230    protected void onLayout(boolean changed, int l, int t, int r, int b) {
231
232        // we layout all our children centered on the top
233        float centerX = getWidth() / 2.0f;
234        for (int i = 0; i < getChildCount(); i++) {
235            View child = getChildAt(i);
236            float width = child.getMeasuredWidth();
237            float height = child.getMeasuredHeight();
238            child.layout((int) (centerX - width / 2.0f),
239                    0,
240                    (int) (centerX + width / 2.0f),
241                    (int) height);
242        }
243        setMaxLayoutHeight(getHeight());
244        updateContentHeight();
245        updateScrollPositionIfNecessary();
246        requestChildrenUpdate();
247    }
248
249    public void updateSpeedBumpIndex(int newIndex) {
250        int currentIndex = indexOfChild(mSpeedBumpView);
251
252        // If we are currently layouted before the new speed bump index, we have to decrease it.
253        boolean validIndex = newIndex > 0;
254        if (newIndex > getChildCount() - 1) {
255            validIndex = false;
256            newIndex = -1;
257        }
258        if (validIndex && currentIndex != newIndex) {
259            changeViewPosition(mSpeedBumpView, newIndex);
260        }
261        updateSpeedBump(validIndex);
262        mAmbientState.setSpeedBumpIndex(newIndex);
263    }
264
265    public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
266        mListener = listener;
267    }
268
269    /**
270     * Returns the location the given child is currently rendered at.
271     *
272     * @param child the child to get the location for
273     * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants
274     */
275    public int getChildLocation(View child) {
276        ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child);
277        if (childViewState == null) {
278            return ViewState.LOCATION_UNKNOWN;
279        }
280        return childViewState.location;
281    }
282
283    private void setMaxLayoutHeight(int maxLayoutHeight) {
284        mMaxLayoutHeight = maxLayoutHeight;
285        updateAlgorithmHeightAndPadding();
286    }
287
288    private void updateAlgorithmHeightAndPadding() {
289        mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
290        mStackScrollAlgorithm.setTopPadding(mTopPadding);
291    }
292
293    /**
294     * @return whether the height of the layout needs to be adapted, in order to ensure that the
295     *         last child is not in the bottom stack.
296     */
297    private boolean needsHeightAdaption() {
298        return getNotGoneChildCount() > 1;
299    }
300
301    private boolean isViewExpanded(View view) {
302        if (view != null) {
303            ExpandableView expandView = (ExpandableView) view;
304            return expandView.getActualHeight() > mCollapsedSize;
305        }
306        return false;
307    }
308
309    /**
310     * Updates the children views according to the stack scroll algorithm. Call this whenever
311     * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
312     */
313    private void updateChildren() {
314        mAmbientState.setScrollY(mOwnScrollY);
315        mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
316        if (!isCurrentlyAnimating() && !mNeedsAnimation) {
317            applyCurrentState();
318        } else {
319            startAnimationToState();
320        }
321    }
322
323    private void requestChildrenUpdate() {
324        if (!mChildrenUpdateRequested) {
325            getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
326            mChildrenUpdateRequested = true;
327            invalidate();
328        }
329    }
330
331    private boolean isCurrentlyAnimating() {
332        return mStateAnimator.isRunning();
333    }
334
335    private void updateScrollPositionIfNecessary() {
336        int scrollRange = getScrollRange();
337        if (scrollRange < mOwnScrollY) {
338            mOwnScrollY = scrollRange;
339        }
340    }
341
342    public int getTopPadding() {
343        return mTopPadding;
344    }
345
346    public void setTopPadding(int topPadding, boolean animate) {
347        if (mTopPadding != topPadding) {
348            mTopPadding = topPadding;
349            updateAlgorithmHeightAndPadding();
350            updateContentHeight();
351            if (animate && mAnimationsEnabled && mIsExpanded) {
352                mTopPaddingNeedsAnimation = true;
353                mNeedsAnimation =  true;
354            }
355            requestChildrenUpdate();
356            if (mOnHeightChangedListener != null) {
357                mOnHeightChangedListener.onHeightChanged(null);
358            }
359        }
360    }
361
362    /**
363     * Update the height of the stack to a new height.
364     *
365     * @param height the new height of the stack
366     */
367    public void setStackHeight(float height) {
368        setIsExpanded(height > 0.0f);
369        int newStackHeight = (int) height;
370        int itemHeight = getItemHeight();
371        int bottomStackPeekSize = mBottomStackPeekSize;
372        int minStackHeight = itemHeight + bottomStackPeekSize;
373        int stackHeight;
374        if (newStackHeight - mTopPadding >= minStackHeight) {
375            setTranslationY(0);
376            stackHeight = newStackHeight;
377        } else {
378
379            // We did not reach the position yet where we actually start growing,
380            // so we translate the stack upwards.
381            int translationY = (newStackHeight - minStackHeight);
382            // A slight parallax effect is introduced in order for the stack to catch up with
383            // the top card.
384            float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight;
385            partiallyThere = Math.max(0, partiallyThere);
386            translationY += (1 - partiallyThere) * bottomStackPeekSize;
387            setTranslationY(translationY - mTopPadding);
388            stackHeight = (int) (height - (translationY - mTopPadding));
389        }
390        if (stackHeight != mCurrentStackHeight) {
391            mCurrentStackHeight = stackHeight;
392            updateAlgorithmHeightAndPadding();
393            requestChildrenUpdate();
394        }
395    }
396
397    /**
398     * Get the current height of the view. This is at most the msize of the view given by a the
399     * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
400     *
401     * @return either the layout height or the externally defined height, whichever is smaller
402     */
403    private int getLayoutHeight() {
404        return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
405    }
406
407    public int getItemHeight() {
408        return mCollapsedSize;
409    }
410
411    public int getBottomStackPeekSize() {
412        return mBottomStackPeekSize;
413    }
414
415    public void setLongPressListener(View.OnLongClickListener listener) {
416        mSwipeHelper.setLongPressListener(listener);
417    }
418
419    public void onChildDismissed(View v) {
420        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
421        final View veto = v.findViewById(R.id.veto);
422        if (veto != null && veto.getVisibility() != View.GONE) {
423            veto.performClick();
424        }
425        setSwipingInProgress(false);
426        if (mDragAnimPendingChildren.contains(v)) {
427            // We start the swipe and finish it in the same frame, we don't want any animation
428            // for the drag
429            mDragAnimPendingChildren.remove(v);
430        }
431        mSwipedOutViews.add(v);
432        mAmbientState.onDragFinished(v);
433    }
434
435    @Override
436    public void onChildSnappedBack(View animView) {
437        mAmbientState.onDragFinished(animView);
438        if (!mDragAnimPendingChildren.contains(animView)) {
439            if (mAnimationsEnabled) {
440                mSnappedBackChildren.add(animView);
441                mNeedsAnimation = true;
442            }
443            requestChildrenUpdate();
444        } else {
445            // We start the swipe and snap back in the same frame, we don't want any animation
446            mDragAnimPendingChildren.remove(animView);
447        }
448    }
449
450    @Override
451    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
452        return false;
453    }
454
455    public void onBeginDrag(View v) {
456        setSwipingInProgress(true);
457        mAmbientState.onBeginDrag(v);
458        if (mAnimationsEnabled) {
459            mDragAnimPendingChildren.add(v);
460            mNeedsAnimation = true;
461        }
462        requestChildrenUpdate();
463    }
464
465    public void onDragCancelled(View v) {
466        setSwipingInProgress(false);
467    }
468
469    public View getChildAtPosition(MotionEvent ev) {
470        return getChildAtPosition(ev.getX(), ev.getY());
471    }
472
473    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
474        int[] location = new int[2];
475        getLocationOnScreen(location);
476        return getChildAtPosition(touchX - location[0], touchY - location[1]);
477    }
478
479    public ExpandableView getChildAtPosition(float touchX, float touchY) {
480        // find the view under the pointer, accounting for GONE views
481        final int count = getChildCount();
482        for (int childIdx = 0; childIdx < count; childIdx++) {
483            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
484            if (slidingChild.getVisibility() == GONE) {
485                continue;
486            }
487            float top = slidingChild.getTranslationY();
488            float bottom = top + slidingChild.getActualHeight();
489            int left = slidingChild.getLeft();
490            int right = slidingChild.getRight();
491
492            if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
493                return slidingChild;
494            }
495        }
496        return null;
497    }
498
499    public boolean canChildBeExpanded(View v) {
500        return v instanceof ExpandableNotificationRow
501                && ((ExpandableNotificationRow) v).isExpandable();
502    }
503
504    public void setUserExpandedChild(View v, boolean userExpanded) {
505        if (v instanceof ExpandableNotificationRow) {
506            ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
507        }
508    }
509
510    public void setUserLockedChild(View v, boolean userLocked) {
511        if (v instanceof ExpandableNotificationRow) {
512            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
513        }
514    }
515
516    public View getChildContentView(View v) {
517        return v;
518    }
519
520    public boolean canChildBeDismissed(View v) {
521        final View veto = v.findViewById(R.id.veto);
522        return (veto != null && veto.getVisibility() != View.GONE);
523    }
524
525    private void setSwipingInProgress(boolean isSwiped) {
526        mSwipingInProgress = isSwiped;
527        if(isSwiped) {
528            requestDisallowInterceptTouchEvent(true);
529        }
530    }
531
532    @Override
533    protected void onConfigurationChanged(Configuration newConfig) {
534        super.onConfigurationChanged(newConfig);
535        float densityScale = getResources().getDisplayMetrics().density;
536        mSwipeHelper.setDensityScale(densityScale);
537        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
538        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
539        initView(getContext());
540    }
541
542    public void dismissRowAnimated(View child, int vel) {
543        mSwipeHelper.dismissChild(child, vel);
544    }
545
546    @Override
547    public boolean onTouchEvent(MotionEvent ev) {
548        if (!isEnabled()) {
549            return false;
550        }
551        boolean scrollerWantsIt = false;
552        if (!mSwipingInProgress) {
553            scrollerWantsIt = onScrollTouch(ev);
554        }
555        boolean horizontalSwipeWantsIt = false;
556        if (!mIsBeingDragged) {
557            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
558        }
559        return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev);
560    }
561
562    private boolean onScrollTouch(MotionEvent ev) {
563        initVelocityTrackerIfNotExists();
564        mVelocityTracker.addMovement(ev);
565
566        final int action = ev.getAction();
567
568        switch (action & MotionEvent.ACTION_MASK) {
569            case MotionEvent.ACTION_DOWN: {
570                if (getChildCount() == 0 || !isInContentBounds(ev)) {
571                    return false;
572                }
573                boolean isBeingDragged = !mScroller.isFinished();
574                setIsBeingDragged(isBeingDragged);
575
576                /*
577                 * If being flinged and user touches, stop the fling. isFinished
578                 * will be false if being flinged.
579                 */
580                if (!mScroller.isFinished()) {
581                    mScroller.forceFinished(true);
582                }
583
584                // Remember where the motion event started
585                mLastMotionY = (int) ev.getY();
586                mActivePointerId = ev.getPointerId(0);
587                break;
588            }
589            case MotionEvent.ACTION_MOVE:
590                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
591                if (activePointerIndex == -1) {
592                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
593                    break;
594                }
595
596                final int y = (int) ev.getY(activePointerIndex);
597                int deltaY = mLastMotionY - y;
598                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
599                    setIsBeingDragged(true);
600                    if (deltaY > 0) {
601                        deltaY -= mTouchSlop;
602                    } else {
603                        deltaY += mTouchSlop;
604                    }
605                }
606                if (mIsBeingDragged) {
607                    // Scroll to follow the motion event
608                    mLastMotionY = y;
609                    final int range = getScrollRange();
610
611                    float scrollAmount;
612                    if (deltaY < 0) {
613                        scrollAmount = overScrollDown(deltaY);
614                    } else {
615                        scrollAmount = overScrollUp(deltaY, range);
616                    }
617
618                    // Calling overScrollBy will call onOverScrolled, which
619                    // calls onScrollChanged if applicable.
620                    if (scrollAmount != 0.0f) {
621                        // The scrolling motion could not be compensated with the
622                        // existing overScroll, we have to scroll the view
623                        overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY,
624                                0, range, 0, getHeight() / 2, true);
625                    }
626                }
627                break;
628            case MotionEvent.ACTION_UP:
629                if (mIsBeingDragged) {
630                    final VelocityTracker velocityTracker = mVelocityTracker;
631                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
632                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
633
634                    if (getChildCount() > 0) {
635                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
636                            fling(-initialVelocity);
637                        } else {
638                            if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
639                                    getScrollRange())) {
640                                postInvalidateOnAnimation();
641                            }
642                        }
643                    }
644
645                    mActivePointerId = INVALID_POINTER;
646                    endDrag();
647                }
648                break;
649            case MotionEvent.ACTION_CANCEL:
650                if (mIsBeingDragged && getChildCount() > 0) {
651                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
652                        postInvalidateOnAnimation();
653                    }
654                    mActivePointerId = INVALID_POINTER;
655                    endDrag();
656                }
657                break;
658            case MotionEvent.ACTION_POINTER_DOWN: {
659                final int index = ev.getActionIndex();
660                mLastMotionY = (int) ev.getY(index);
661                mActivePointerId = ev.getPointerId(index);
662                break;
663            }
664            case MotionEvent.ACTION_POINTER_UP:
665                onSecondaryPointerUp(ev);
666                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
667                break;
668        }
669        return true;
670    }
671
672    /**
673     * Perform a scroll upwards and adapt the overscroll amounts accordingly
674     *
675     * @param deltaY The amount to scroll upwards, has to be positive.
676     * @return The amount of scrolling to be performed by the scroller,
677     *         not handled by the overScroll amount.
678     */
679    private float overScrollUp(int deltaY, int range) {
680        deltaY = Math.max(deltaY, 0);
681        float currentTopAmount = getCurrentOverScrollAmount(true);
682        float newTopAmount = currentTopAmount - deltaY;
683        if (currentTopAmount > 0) {
684            setOverScrollAmount(newTopAmount, true /* onTop */,
685                    false /* animate */);
686        }
687        // Top overScroll might not grab all scrolling motion,
688        // we have to scroll as well.
689        float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
690        float newScrollY = mOwnScrollY + scrollAmount;
691        if (newScrollY > range) {
692            float currentBottomPixels = getCurrentOverScrolledPixels(false);
693            // We overScroll on the top
694            setOverScrolledPixels(currentBottomPixels + newScrollY - range,
695                    false /* onTop */,
696                    false /* animate */);
697            mOwnScrollY = range;
698            scrollAmount = 0.0f;
699        }
700        return scrollAmount;
701    }
702
703    /**
704     * Perform a scroll downward and adapt the overscroll amounts accordingly
705     *
706     * @param deltaY The amount to scroll downwards, has to be negative.
707     * @return The amount of scrolling to be performed by the scroller,
708     *         not handled by the overScroll amount.
709     */
710    private float overScrollDown(int deltaY) {
711        deltaY = Math.min(deltaY, 0);
712        float currentBottomAmount = getCurrentOverScrollAmount(false);
713        float newBottomAmount = currentBottomAmount + deltaY;
714        if (currentBottomAmount > 0) {
715            setOverScrollAmount(newBottomAmount, false /* onTop */,
716                    false /* animate */);
717        }
718        // Bottom overScroll might not grab all scrolling motion,
719        // we have to scroll as well.
720        float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
721        float newScrollY = mOwnScrollY + scrollAmount;
722        if (newScrollY < 0) {
723            float currentTopPixels = getCurrentOverScrolledPixels(true);
724            // We overScroll on the top
725            setOverScrolledPixels(currentTopPixels - newScrollY,
726                    true /* onTop */,
727                    false /* animate */);
728            mOwnScrollY = 0;
729            scrollAmount = 0.0f;
730        }
731        return scrollAmount;
732    }
733
734    private void onSecondaryPointerUp(MotionEvent ev) {
735        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
736                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
737        final int pointerId = ev.getPointerId(pointerIndex);
738        if (pointerId == mActivePointerId) {
739            // This was our active pointer going up. Choose a new
740            // active pointer and adjust accordingly.
741            // TODO: Make this decision more intelligent.
742            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
743            mLastMotionY = (int) ev.getY(newPointerIndex);
744            mActivePointerId = ev.getPointerId(newPointerIndex);
745            if (mVelocityTracker != null) {
746                mVelocityTracker.clear();
747            }
748        }
749    }
750
751    private void initVelocityTrackerIfNotExists() {
752        if (mVelocityTracker == null) {
753            mVelocityTracker = VelocityTracker.obtain();
754        }
755    }
756
757    private void recycleVelocityTracker() {
758        if (mVelocityTracker != null) {
759            mVelocityTracker.recycle();
760            mVelocityTracker = null;
761        }
762    }
763
764    private void initOrResetVelocityTracker() {
765        if (mVelocityTracker == null) {
766            mVelocityTracker = VelocityTracker.obtain();
767        } else {
768            mVelocityTracker.clear();
769        }
770    }
771
772    @Override
773    public void computeScroll() {
774        if (mScroller.computeScrollOffset()) {
775            // This is called at drawing time by ViewGroup.
776            int oldX = mScrollX;
777            int oldY = mOwnScrollY;
778            int x = mScroller.getCurrX();
779            int y = mScroller.getCurrY();
780
781            if (oldX != x || oldY != y) {
782                final int range = getScrollRange();
783                if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
784                    float currVelocity = mScroller.getCurrVelocity();
785                    if (currVelocity >= mMinimumVelocity) {
786                        mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
787                    }
788                }
789
790                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
791                        0, (int) (mMaxOverScroll), false);
792                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
793            }
794
795            // Keep on drawing until the animation has finished.
796            postInvalidateOnAnimation();
797        }
798    }
799
800    @Override
801    protected boolean overScrollBy(int deltaX, int deltaY,
802            int scrollX, int scrollY,
803            int scrollRangeX, int scrollRangeY,
804            int maxOverScrollX, int maxOverScrollY,
805            boolean isTouchEvent) {
806
807        int newScrollY = scrollY + deltaY;
808
809        final int top = -maxOverScrollY;
810        final int bottom = maxOverScrollY + scrollRangeY;
811
812        boolean clampedY = false;
813        if (newScrollY > bottom) {
814            newScrollY = bottom;
815            clampedY = true;
816        } else if (newScrollY < top) {
817            newScrollY = top;
818            clampedY = true;
819        }
820
821        onOverScrolled(0, newScrollY, false, clampedY);
822
823        return clampedY;
824    }
825
826    /**
827     * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded
828     * overscroll effect based on numPixels. By default this will also cancel animations on the
829     * same overScroll edge.
830     *
831     * @param numPixels The amount of pixels to overScroll by. These will be scaled according to
832     *                  the rubber-banding logic.
833     * @param onTop Should the effect be applied on top of the scroller.
834     * @param animate Should an animation be performed.
835     */
836    public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) {
837        setOverScrollAmount(numPixels * RUBBER_BAND_FACTOR, onTop, animate, true);
838    }
839
840    /**
841     * Set the effective overScroll amount which will be directly reflected in the layout.
842     * By default this will also cancel animations on the same overScroll edge.
843     *
844     * @param amount The amount to overScroll by.
845     * @param onTop Should the effect be applied on top of the scroller.
846     * @param animate Should an animation be performed.
847     */
848    public void setOverScrollAmount(float amount, boolean onTop, boolean animate) {
849        setOverScrollAmount(amount, onTop, animate, true);
850    }
851
852    /**
853     * Set the effective overScroll amount which will be directly reflected in the layout.
854     *
855     * @param amount The amount to overScroll by.
856     * @param onTop Should the effect be applied on top of the scroller.
857     * @param animate Should an animation be performed.
858     * @param cancelAnimators Should running animations be cancelled.
859     */
860    public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
861            boolean cancelAnimators) {
862        if (cancelAnimators) {
863            mStateAnimator.cancelOverScrollAnimators(onTop);
864        }
865        setOverScrollAmountInternal(amount, onTop, animate);
866    }
867
868    private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate) {
869        amount = Math.max(0, amount);
870        if (animate) {
871            mStateAnimator.animateOverScrollToAmount(amount, onTop);
872        } else {
873            setOverScrolledPixels(amount / RUBBER_BAND_FACTOR, onTop);
874            mAmbientState.setOverScrollAmount(amount, onTop);
875            requestChildrenUpdate();
876            if (onTop) {
877                float scrollAmount = mOwnScrollY < 0 ? -mOwnScrollY : 0;
878                notifyOverscrollTopListener(scrollAmount + amount);
879            }
880        }
881    }
882
883    private void notifyOverscrollTopListener(float amount) {
884        if (mOverscrollTopChangedListener != null) {
885            mOverscrollTopChangedListener.onOverscrollTopChanged(amount);
886        }
887    }
888
889    public void setOverscrollTopChangedListener(
890            OnOverscrollTopChangedListener overscrollTopChangedListener) {
891        mOverscrollTopChangedListener = overscrollTopChangedListener;
892    }
893
894    public float getCurrentOverScrollAmount(boolean top) {
895        return mAmbientState.getOverScrollAmount(top);
896    }
897
898    public float getCurrentOverScrolledPixels(boolean top) {
899        return top? mOverScrolledTopPixels : mOverScrolledBottomPixels;
900    }
901
902    private void setOverScrolledPixels(float amount, boolean onTop) {
903        if (onTop) {
904            mOverScrolledTopPixels = amount;
905        } else {
906            mOverScrolledBottomPixels = amount;
907        }
908    }
909
910    private void customScrollTo(int y) {
911        mOwnScrollY = y;
912        updateChildren();
913    }
914
915    @Override
916    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
917        // Treat animating scrolls differently; see #computeScroll() for why.
918        if (!mScroller.isFinished()) {
919            final int oldX = mScrollX;
920            final int oldY = mOwnScrollY;
921            mScrollX = scrollX;
922            mOwnScrollY = scrollY;
923            if (clampedY) {
924                springBack();
925            } else {
926                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
927                invalidateParentIfNeeded();
928                updateChildren();
929                float overScrollTop = getCurrentOverScrollAmount(true);
930                if (mOwnScrollY < 0) {
931                    notifyOverscrollTopListener(-mOwnScrollY + overScrollTop);
932                } else {
933                    notifyOverscrollTopListener(overScrollTop);
934                }
935            }
936        } else {
937            customScrollTo(scrollY);
938            scrollTo(scrollX, mScrollY);
939        }
940    }
941
942    private void springBack() {
943        int scrollRange = getScrollRange();
944        boolean overScrolledTop = mOwnScrollY <= 0;
945        boolean overScrolledBottom = mOwnScrollY >= scrollRange;
946        if (overScrolledTop || overScrolledBottom) {
947            boolean onTop;
948            float newAmount;
949            if (overScrolledTop) {
950                onTop = true;
951                newAmount = -mOwnScrollY;
952                mOwnScrollY = 0;
953            } else {
954                onTop = false;
955                newAmount = mOwnScrollY - scrollRange;
956                mOwnScrollY = scrollRange;
957            }
958            setOverScrollAmount(newAmount, onTop, false);
959            setOverScrollAmount(0.0f, onTop, true);
960            mScroller.forceFinished(true);
961        }
962    }
963
964    private int getScrollRange() {
965        int scrollRange = 0;
966        ExpandableView firstChild = (ExpandableView) getFirstChildNotGone();
967        if (firstChild != null) {
968            int contentHeight = getContentHeight();
969            int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild);
970            scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize
971                    + mBottomStackSlowDownHeight);
972            if (scrollRange > 0) {
973                View lastChild = getLastChildNotGone();
974                // We want to at least be able collapse the first item and not ending in a weird
975                // end state.
976                scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize);
977            }
978        }
979        return scrollRange;
980    }
981
982    /**
983     * @return the first child which has visibility unequal to GONE
984     */
985    private View getFirstChildNotGone() {
986        int childCount = getChildCount();
987        for (int i = 0; i < childCount; i++) {
988            View child = getChildAt(i);
989            if (child.getVisibility() != View.GONE) {
990                return child;
991            }
992        }
993        return null;
994    }
995
996    /**
997     * @return The first child which has visibility unequal to GONE which is currently below the
998     *         given translationY or equal to it.
999     */
1000    private View getFirstChildBelowTranlsationY(float translationY) {
1001        int childCount = getChildCount();
1002        for (int i = 0; i < childCount; i++) {
1003            View child = getChildAt(i);
1004            if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) {
1005                return child;
1006            }
1007        }
1008        return null;
1009    }
1010
1011    /**
1012     * @return the last child which has visibility unequal to GONE
1013     */
1014    public View getLastChildNotGone() {
1015        int childCount = getChildCount();
1016        for (int i = childCount - 1; i >= 0; i--) {
1017            View child = getChildAt(i);
1018            if (child.getVisibility() != View.GONE) {
1019                return child;
1020            }
1021        }
1022        return null;
1023    }
1024
1025    /**
1026     * @return the number of children which have visibility unequal to GONE
1027     */
1028    public int getNotGoneChildCount() {
1029        int childCount = getChildCount();
1030        int count = 0;
1031        for (int i = 0; i < childCount; i++) {
1032            View child = getChildAt(i);
1033            if (child.getVisibility() != View.GONE) {
1034                count++;
1035            }
1036        }
1037        return count;
1038    }
1039
1040    private int getMaxExpandHeight(View view) {
1041        if (view instanceof ExpandableNotificationRow) {
1042            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
1043            return row.getIntrinsicHeight();
1044        }
1045        return view.getHeight();
1046    }
1047
1048    private int getContentHeight() {
1049        return mContentHeight;
1050    }
1051
1052    private void updateContentHeight() {
1053        int height = 0;
1054        for (int i = 0; i < getChildCount(); i++) {
1055            View child = getChildAt(i);
1056            if (child.getVisibility() != View.GONE) {
1057                if (height != 0) {
1058                    // add the padding before this element
1059                    height += mPaddingBetweenElements;
1060                }
1061                if (child instanceof ExpandableNotificationRow) {
1062                    ExpandableNotificationRow row = (ExpandableNotificationRow) child;
1063                    height += row.getIntrinsicHeight();
1064                } else if (child instanceof ExpandableView) {
1065                    ExpandableView expandableView = (ExpandableView) child;
1066                    height += expandableView.getActualHeight();
1067                }
1068            }
1069        }
1070        mContentHeight = height + mTopPadding;
1071    }
1072
1073    /**
1074     * Fling the scroll view
1075     *
1076     * @param velocityY The initial velocity in the Y direction. Positive
1077     *                  numbers mean that the finger/cursor is moving down the screen,
1078     *                  which means we want to scroll towards the top.
1079     */
1080    private void fling(int velocityY) {
1081        if (getChildCount() > 0) {
1082            int scrollRange = getScrollRange();
1083
1084            float topAmount = getCurrentOverScrollAmount(true);
1085            float bottomAmount = getCurrentOverScrollAmount(false);
1086            if (velocityY < 0 && topAmount > 0) {
1087                mOwnScrollY -= (int) topAmount;
1088                setOverScrollAmount(0, true, false);
1089                mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR
1090                        * mOverflingDistance + topAmount;
1091            } else if (velocityY > 0 && bottomAmount > 0) {
1092                mOwnScrollY += bottomAmount;
1093                setOverScrollAmount(0, false, false);
1094                mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR
1095                        * mOverflingDistance + bottomAmount;
1096            } else {
1097                // it will be set once we reach the boundary
1098                mMaxOverScroll = 0.0f;
1099            }
1100            mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0,
1101                    Math.max(0, scrollRange), 0, Integer.MAX_VALUE / 2);
1102
1103            postInvalidateOnAnimation();
1104        }
1105    }
1106
1107    private void endDrag() {
1108        setIsBeingDragged(false);
1109
1110        recycleVelocityTracker();
1111
1112        if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
1113            setOverScrollAmount(0, true /* onTop */, true /* animate */);
1114        }
1115        if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
1116            setOverScrollAmount(0, false /* onTop */, true /* animate */);
1117        }
1118    }
1119
1120    @Override
1121    public boolean onInterceptTouchEvent(MotionEvent ev) {
1122        boolean scrollWantsIt = false;
1123        if (!mSwipingInProgress) {
1124            scrollWantsIt = onInterceptTouchEventScroll(ev);
1125        }
1126        boolean swipeWantsIt = false;
1127        if (!mIsBeingDragged) {
1128            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
1129        }
1130        return swipeWantsIt || scrollWantsIt ||
1131                super.onInterceptTouchEvent(ev);
1132    }
1133
1134    @Override
1135    protected void onViewRemoved(View child) {
1136        super.onViewRemoved(child);
1137        mStackScrollAlgorithm.notifyChildrenChanged(this);
1138        if (mChildrenChangingPositions.contains(child)) {
1139            // This is only a position change, don't do anything special
1140            return;
1141        }
1142        ((ExpandableView) child).setOnHeightChangedListener(null);
1143        mCurrentStackScrollState.removeViewStateForView(child);
1144        updateScrollStateForRemovedChild(child);
1145        boolean animationGenerated = generateRemoveAnimation(child);
1146        if (animationGenerated && !mSwipedOutViews.contains(child)) {
1147            // Add this view to an overlay in order to ensure that it will still be temporary
1148            // drawn when removed
1149            getOverlay().add(child);
1150        }
1151    }
1152
1153    /**
1154     * Generate a remove animation for a child view.
1155     *
1156     * @param child The view to generate the remove animation for.
1157     * @return Whether an animation was generated.
1158     */
1159    private boolean generateRemoveAnimation(View child) {
1160        if (mIsExpanded && mAnimationsEnabled) {
1161            if (!mChildrenToAddAnimated.contains(child)) {
1162                // Generate Animations
1163                mChildrenToRemoveAnimated.add(child);
1164                mNeedsAnimation = true;
1165                return true;
1166            } else {
1167                mChildrenToAddAnimated.remove(child);
1168                return false;
1169            }
1170        }
1171        return false;
1172    }
1173
1174    /**
1175     * Updates the scroll position when a child was removed
1176     *
1177     * @param removedChild the removed child
1178     */
1179    private void updateScrollStateForRemovedChild(View removedChild) {
1180        int startingPosition = getPositionInLinearLayout(removedChild);
1181        int childHeight = removedChild.getHeight() + mPaddingBetweenElements;
1182        int endPosition = startingPosition + childHeight;
1183        if (endPosition <= mOwnScrollY) {
1184            // This child is fully scrolled of the top, so we have to deduct its height from the
1185            // scrollPosition
1186            mOwnScrollY -= childHeight;
1187        } else if (startingPosition < mOwnScrollY) {
1188            // This child is currently being scrolled into, set the scroll position to the start of
1189            // this child
1190            mOwnScrollY = startingPosition;
1191        }
1192    }
1193
1194    private int getPositionInLinearLayout(View requestedChild) {
1195        int position = 0;
1196        for (int i = 0; i < getChildCount(); i++) {
1197            View child = getChildAt(i);
1198            if (child == requestedChild) {
1199                return position;
1200            }
1201            if (child.getVisibility() != View.GONE) {
1202                position += child.getHeight();
1203                if (i < getChildCount()-1) {
1204                    position += mPaddingBetweenElements;
1205                }
1206            }
1207        }
1208        return 0;
1209    }
1210
1211    @Override
1212    protected void onViewAdded(View child) {
1213        super.onViewAdded(child);
1214        mStackScrollAlgorithm.notifyChildrenChanged(this);
1215        ((ExpandableView) child).setOnHeightChangedListener(this);
1216        generateAddAnimation(child);
1217    }
1218
1219    public void setAnimationsEnabled(boolean animationsEnabled) {
1220        mAnimationsEnabled = animationsEnabled;
1221    }
1222
1223    public boolean isAddOrRemoveAnimationPending() {
1224        return mNeedsAnimation
1225                && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
1226    }
1227    /**
1228     * Generate an animation for an added child view.
1229     *
1230     * @param child The view to be added.
1231     */
1232    public void generateAddAnimation(View child) {
1233        if (mIsExpanded && mAnimationsEnabled && !mChildrenChangingPositions.contains(child)) {
1234            // Generate Animations
1235            mChildrenToAddAnimated.add(child);
1236            mNeedsAnimation = true;
1237        }
1238    }
1239
1240    /**
1241     * Change the position of child to a new location
1242     *
1243     * @param child the view to change the position for
1244     * @param newIndex the new index
1245     */
1246    public void changeViewPosition(View child, int newIndex) {
1247        if (child != null && child.getParent() == this) {
1248            mChildrenChangingPositions.add(child);
1249            removeView(child);
1250            addView(child, newIndex);
1251            mNeedsAnimation = true;
1252        }
1253    }
1254
1255    private void startAnimationToState() {
1256        if (mNeedsAnimation) {
1257            generateChildHierarchyEvents();
1258            mNeedsAnimation = false;
1259        }
1260        if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
1261            mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState);
1262            mAnimationEvents.clear();
1263        } else {
1264            applyCurrentState();
1265        }
1266    }
1267
1268    private void generateChildHierarchyEvents() {
1269        generateChildRemovalEvents();
1270        generateChildAdditionEvents();
1271        generatePositionChangeEvents();
1272        generateSnapBackEvents();
1273        generateDragEvents();
1274        generateTopPaddingEvent();
1275        generateActivateEvent();
1276        generateDimmedEvent();
1277        mNeedsAnimation = false;
1278    }
1279
1280    private void generateSnapBackEvents() {
1281        for (View child : mSnappedBackChildren) {
1282            mAnimationEvents.add(new AnimationEvent(child,
1283                    AnimationEvent.ANIMATION_TYPE_SNAP_BACK));
1284        }
1285        mSnappedBackChildren.clear();
1286    }
1287
1288    private void generateDragEvents() {
1289        for (View child : mDragAnimPendingChildren) {
1290            mAnimationEvents.add(new AnimationEvent(child,
1291                    AnimationEvent.ANIMATION_TYPE_START_DRAG));
1292        }
1293        mDragAnimPendingChildren.clear();
1294    }
1295
1296    private void generateChildRemovalEvents() {
1297        for (View child : mChildrenToRemoveAnimated) {
1298            boolean childWasSwipedOut = mSwipedOutViews.contains(child);
1299            int animationType = childWasSwipedOut
1300                    ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
1301                    : AnimationEvent.ANIMATION_TYPE_REMOVE;
1302            AnimationEvent event = new AnimationEvent(child, animationType);
1303
1304            // we need to know the view after this one
1305            event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY());
1306            mAnimationEvents.add(event);
1307        }
1308        mSwipedOutViews.clear();
1309        mChildrenToRemoveAnimated.clear();
1310    }
1311
1312    private void generatePositionChangeEvents() {
1313        for (View child : mChildrenChangingPositions) {
1314            mAnimationEvents.add(new AnimationEvent(child,
1315                    AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
1316        }
1317        mChildrenChangingPositions.clear();
1318    }
1319
1320    private void generateChildAdditionEvents() {
1321        for (View child : mChildrenToAddAnimated) {
1322            mAnimationEvents.add(new AnimationEvent(child,
1323                    AnimationEvent.ANIMATION_TYPE_ADD));
1324        }
1325        mChildrenToAddAnimated.clear();
1326    }
1327
1328    private void generateTopPaddingEvent() {
1329        if (mTopPaddingNeedsAnimation) {
1330            mAnimationEvents.add(
1331                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED));
1332        }
1333        mTopPaddingNeedsAnimation = false;
1334    }
1335
1336    private void generateActivateEvent() {
1337        if (mActivateNeedsAnimation) {
1338            mAnimationEvents.add(
1339                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD));
1340        }
1341        mActivateNeedsAnimation = false;
1342    }
1343
1344    private void generateDimmedEvent() {
1345        if (mDimmedNeedsAnimation) {
1346            mAnimationEvents.add(
1347                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED));
1348        }
1349        mDimmedNeedsAnimation = false;
1350    }
1351
1352    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
1353        /*
1354         * This method JUST determines whether we want to intercept the motion.
1355         * If we return true, onMotionEvent will be called and we do the actual
1356         * scrolling there.
1357         */
1358
1359        /*
1360        * Shortcut the most recurring case: the user is in the dragging
1361        * state and he is moving his finger.  We want to intercept this
1362        * motion.
1363        */
1364        final int action = ev.getAction();
1365        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
1366            return true;
1367        }
1368
1369        /*
1370         * Don't try to intercept touch if we can't scroll anyway.
1371         */
1372        if (mOwnScrollY == 0 && getScrollRange() == 0) {
1373            return false;
1374        }
1375
1376        switch (action & MotionEvent.ACTION_MASK) {
1377            case MotionEvent.ACTION_MOVE: {
1378                /*
1379                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
1380                 * whether the user has moved far enough from his original down touch.
1381                 */
1382
1383                /*
1384                * Locally do absolute value. mLastMotionY is set to the y value
1385                * of the down event.
1386                */
1387                final int activePointerId = mActivePointerId;
1388                if (activePointerId == INVALID_POINTER) {
1389                    // If we don't have a valid id, the touch down wasn't on content.
1390                    break;
1391                }
1392
1393                final int pointerIndex = ev.findPointerIndex(activePointerId);
1394                if (pointerIndex == -1) {
1395                    Log.e(TAG, "Invalid pointerId=" + activePointerId
1396                            + " in onInterceptTouchEvent");
1397                    break;
1398                }
1399
1400                final int y = (int) ev.getY(pointerIndex);
1401                final int yDiff = Math.abs(y - mLastMotionY);
1402                if (yDiff > mTouchSlop) {
1403                    setIsBeingDragged(true);
1404                    mLastMotionY = y;
1405                    initVelocityTrackerIfNotExists();
1406                    mVelocityTracker.addMovement(ev);
1407                }
1408                break;
1409            }
1410
1411            case MotionEvent.ACTION_DOWN: {
1412                final int y = (int) ev.getY();
1413                if (getChildAtPosition(ev.getX(), y) == null) {
1414                    setIsBeingDragged(false);
1415                    recycleVelocityTracker();
1416                    break;
1417                }
1418
1419                /*
1420                 * Remember location of down touch.
1421                 * ACTION_DOWN always refers to pointer index 0.
1422                 */
1423                mLastMotionY = y;
1424                mActivePointerId = ev.getPointerId(0);
1425
1426                initOrResetVelocityTracker();
1427                mVelocityTracker.addMovement(ev);
1428                /*
1429                * If being flinged and user touches the screen, initiate drag;
1430                * otherwise don't.  mScroller.isFinished should be false when
1431                * being flinged.
1432                */
1433                boolean isBeingDragged = !mScroller.isFinished();
1434                setIsBeingDragged(isBeingDragged);
1435                break;
1436            }
1437
1438            case MotionEvent.ACTION_CANCEL:
1439            case MotionEvent.ACTION_UP:
1440                /* Release the drag */
1441                setIsBeingDragged(false);
1442                mActivePointerId = INVALID_POINTER;
1443                recycleVelocityTracker();
1444                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
1445                    postInvalidateOnAnimation();
1446                }
1447                break;
1448            case MotionEvent.ACTION_POINTER_UP:
1449                onSecondaryPointerUp(ev);
1450                break;
1451        }
1452
1453        /*
1454        * The only time we want to intercept motion events is if we are in the
1455        * drag mode.
1456        */
1457        return mIsBeingDragged;
1458    }
1459
1460    /**
1461     * @return Whether the specified motion event is actually happening over the content.
1462     */
1463    private boolean isInContentBounds(MotionEvent event) {
1464        return event.getY() < getHeight() - getEmptyBottomMargin();
1465    }
1466
1467    private void setIsBeingDragged(boolean isDragged) {
1468        mIsBeingDragged = isDragged;
1469        if (isDragged) {
1470            requestDisallowInterceptTouchEvent(true);
1471            mSwipeHelper.removeLongPressCallback();
1472        }
1473    }
1474
1475    @Override
1476    public void onWindowFocusChanged(boolean hasWindowFocus) {
1477        super.onWindowFocusChanged(hasWindowFocus);
1478        if (!hasWindowFocus) {
1479            mSwipeHelper.removeLongPressCallback();
1480        }
1481    }
1482
1483    @Override
1484    public boolean isScrolledToTop() {
1485        return mOwnScrollY == 0;
1486    }
1487
1488    @Override
1489    public boolean isScrolledToBottom() {
1490        return mOwnScrollY >= getScrollRange();
1491    }
1492
1493    @Override
1494    public View getHostView() {
1495        return this;
1496    }
1497
1498    public int getEmptyBottomMargin() {
1499        int emptyMargin = mMaxLayoutHeight - mContentHeight;
1500        if (needsHeightAdaption()) {
1501            emptyMargin = emptyMargin - mBottomStackSlowDownHeight - mBottomStackPeekSize;
1502        } else {
1503            emptyMargin = emptyMargin - mBottomStackPeekSize;
1504        }
1505        return Math.max(emptyMargin, 0);
1506    }
1507
1508    public void onExpansionStarted() {
1509        mIsExpansionChanging = true;
1510        mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState);
1511    }
1512
1513    public void onExpansionStopped() {
1514        mIsExpansionChanging = false;
1515        mStackScrollAlgorithm.onExpansionStopped();
1516    }
1517
1518    private void setIsExpanded(boolean isExpanded) {
1519        mIsExpanded = isExpanded;
1520        mStackScrollAlgorithm.setIsExpanded(isExpanded);
1521        if (!isExpanded) {
1522            mOwnScrollY = 0;
1523            mSpeedBumpView.collapse();
1524        }
1525    }
1526
1527    @Override
1528    public void onHeightChanged(ExpandableView view) {
1529        updateContentHeight();
1530        updateScrollPositionIfNecessary();
1531        if (mOnHeightChangedListener != null) {
1532            mOnHeightChangedListener.onHeightChanged(view);
1533        }
1534        requestChildrenUpdate();
1535    }
1536
1537    public void setOnHeightChangedListener(
1538            ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
1539        this.mOnHeightChangedListener = mOnHeightChangedListener;
1540    }
1541
1542    public void onChildAnimationFinished() {
1543        requestChildrenUpdate();
1544    }
1545
1546    /**
1547     * See {@link AmbientState#setDimmed}.
1548     */
1549    public void setDimmed(boolean dimmed, boolean animate) {
1550        mStackScrollAlgorithm.setDimmed(dimmed);
1551        mAmbientState.setDimmed(dimmed);
1552        updatePadding(dimmed);
1553        if (animate && mAnimationsEnabled) {
1554            mDimmedNeedsAnimation = true;
1555            mNeedsAnimation =  true;
1556        }
1557        requestChildrenUpdate();
1558    }
1559
1560    /**
1561     * See {@link AmbientState#setActivatedChild}.
1562     */
1563    public void setActivatedChild(View activatedChild) {
1564        mAmbientState.setActivatedChild(activatedChild);
1565        if (mAnimationsEnabled) {
1566            mActivateNeedsAnimation = true;
1567            mNeedsAnimation =  true;
1568        }
1569        requestChildrenUpdate();
1570    }
1571
1572    public View getActivatedChild() {
1573        return mAmbientState.getActivatedChild();
1574    }
1575
1576    private void applyCurrentState() {
1577        mCurrentStackScrollState.apply();
1578        if (mListener != null) {
1579            mListener.onChildLocationsChanged(this);
1580        }
1581    }
1582
1583    public void setSpeedBumpView(SpeedBumpView speedBumpView) {
1584        mSpeedBumpView = speedBumpView;
1585        addView(speedBumpView);
1586    }
1587
1588    private void updateSpeedBump(boolean visible) {
1589        boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE;
1590        if (visible != notGoneBefore) {
1591            int newVisibility = visible ? VISIBLE : GONE;
1592            mSpeedBumpView.setVisibility(newVisibility);
1593            if (visible) {
1594                mSpeedBumpView.collapse();
1595                // Make invisible to ensure that the appear animation is played.
1596                mSpeedBumpView.setInvisible();
1597                if (!mIsExpansionChanging) {
1598                    generateAddAnimation(mSpeedBumpView);
1599                }
1600            } else {
1601                mSpeedBumpView.performVisibilityAnimation(false);
1602                generateRemoveAnimation(mSpeedBumpView);
1603            }
1604        }
1605    }
1606
1607    public void goToFullShade() {
1608        updateSpeedBump(true);
1609    }
1610
1611    /**
1612     * @return the y position of the first notification
1613     */
1614    public float getNotificationsTopY() {
1615        return mTopPadding + getTranslationY();
1616    }
1617
1618    /**
1619     * A listener that is notified when some child locations might have changed.
1620     */
1621    public interface OnChildLocationsChangedListener {
1622        public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
1623    }
1624
1625    /**
1626     * A listener that gets notified when the overscroll at the top has changed.
1627     */
1628    public interface OnOverscrollTopChangedListener {
1629        public void onOverscrollTopChanged(float amount);
1630    }
1631
1632    static class AnimationEvent {
1633
1634        static AnimationFilter[] FILTERS = new AnimationFilter[] {
1635
1636                // ANIMATION_TYPE_ADD
1637                new AnimationFilter()
1638                        .animateAlpha()
1639                        .animateHeight()
1640                        .animateTopInset()
1641                        .animateY()
1642                        .animateZ()
1643                        .hasDelays(),
1644
1645                // ANIMATION_TYPE_REMOVE
1646                new AnimationFilter()
1647                        .animateAlpha()
1648                        .animateHeight()
1649                        .animateTopInset()
1650                        .animateY()
1651                        .animateZ()
1652                        .hasDelays(),
1653
1654                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
1655                new AnimationFilter()
1656                        .animateAlpha()
1657                        .animateHeight()
1658                        .animateTopInset()
1659                        .animateY()
1660                        .animateZ()
1661                        .hasDelays(),
1662
1663                // ANIMATION_TYPE_TOP_PADDING_CHANGED
1664                new AnimationFilter()
1665                        .animateAlpha()
1666                        .animateHeight()
1667                        .animateTopInset()
1668                        .animateY()
1669                        .animateDimmed()
1670                        .animateScale()
1671                        .animateZ(),
1672
1673                // ANIMATION_TYPE_START_DRAG
1674                new AnimationFilter()
1675                        .animateAlpha(),
1676
1677                // ANIMATION_TYPE_SNAP_BACK
1678                new AnimationFilter()
1679                        .animateAlpha(),
1680
1681                // ANIMATION_TYPE_ACTIVATED_CHILD
1682                new AnimationFilter()
1683                        .animateScale()
1684                        .animateAlpha(),
1685
1686                // ANIMATION_TYPE_DIMMED
1687                new AnimationFilter()
1688                        .animateY()
1689                        .animateScale()
1690                        .animateDimmed(),
1691
1692                // ANIMATION_TYPE_CHANGE_POSITION
1693                new AnimationFilter()
1694                        .animateAlpha()
1695                        .animateHeight()
1696                        .animateTopInset()
1697                        .animateY()
1698                        .animateZ()
1699        };
1700
1701        static int[] LENGTHS = new int[] {
1702
1703                // ANIMATION_TYPE_ADD
1704                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
1705
1706                // ANIMATION_TYPE_REMOVE
1707                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
1708
1709                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
1710                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1711
1712                // ANIMATION_TYPE_TOP_PADDING_CHANGED
1713                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1714
1715                // ANIMATION_TYPE_START_DRAG
1716                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1717
1718                // ANIMATION_TYPE_SNAP_BACK
1719                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1720
1721                // ANIMATION_TYPE_ACTIVATED_CHILD
1722                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
1723
1724                // ANIMATION_TYPE_DIMMED
1725                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
1726
1727                // ANIMATION_TYPE_CHANGE_POSITION
1728                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1729        };
1730
1731        static final int ANIMATION_TYPE_ADD = 0;
1732        static final int ANIMATION_TYPE_REMOVE = 1;
1733        static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
1734        static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
1735        static final int ANIMATION_TYPE_START_DRAG = 4;
1736        static final int ANIMATION_TYPE_SNAP_BACK = 5;
1737        static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
1738        static final int ANIMATION_TYPE_DIMMED = 7;
1739        static final int ANIMATION_TYPE_CHANGE_POSITION = 8;
1740
1741        final long eventStartTime;
1742        final View changingView;
1743        final int animationType;
1744        final AnimationFilter filter;
1745        final long length;
1746        View viewAfterChangingView;
1747
1748        AnimationEvent(View view, int type) {
1749            eventStartTime = AnimationUtils.currentAnimationTimeMillis();
1750            changingView = view;
1751            animationType = type;
1752            filter = FILTERS[type];
1753            length = LENGTHS[type];
1754        }
1755
1756        /**
1757         * Combines the length of several animation events into a single value.
1758         *
1759         * @param events The events of the lengths to combine.
1760         * @return The combined length. This is just the maximum of all length.
1761         */
1762        static long combineLength(ArrayList<AnimationEvent> events) {
1763            long length = 0;
1764            int size = events.size();
1765            for (int i = 0; i < size; i++) {
1766                length = Math.max(length, events.get(i).length);
1767            }
1768            return length;
1769        }
1770    }
1771
1772}
1773