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