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