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