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