NotificationStackScrollLayout.java revision 068f5929d10a2daf93d6a0aa26e48b1185c36c98
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.systemui.statusbar.stack;
18
19import android.content.Context;
20import android.content.res.Configuration;
21
22import android.graphics.Canvas;
23import android.graphics.Outline;
24import android.graphics.Paint;
25
26import android.util.AttributeSet;
27import android.util.Log;
28
29import android.view.MotionEvent;
30import android.view.VelocityTracker;
31import android.view.View;
32import android.view.ViewConfiguration;
33import android.view.ViewGroup;
34import android.widget.OverScroller;
35
36import com.android.systemui.ExpandHelper;
37import com.android.systemui.R;
38import com.android.systemui.SwipeHelper;
39import com.android.systemui.statusbar.ExpandableNotificationRow;
40import com.android.systemui.statusbar.stack.StackScrollState.ViewState;
41import com.android.systemui.statusbar.policy.ScrollAdapter;
42
43/**
44 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
45 */
46public class NotificationStackScrollLayout extends ViewGroup
47        implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter {
48
49    private static final String TAG = "NotificationStackScrollLayout";
50    private static final boolean DEBUG = false;
51
52    /**
53     * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
54     */
55    private static final int INVALID_POINTER = -1;
56
57    private SwipeHelper mSwipeHelper;
58    private boolean mSwipingInProgress = true;
59    private int mCurrentStackHeight = Integer.MAX_VALUE;
60    private int mOwnScrollY;
61    private int mMaxLayoutHeight;
62
63    private VelocityTracker mVelocityTracker;
64    private OverScroller mScroller;
65    private int mTouchSlop;
66    private int mMinimumVelocity;
67    private int mMaximumVelocity;
68    private int mOverscrollDistance;
69    private int mOverflingDistance;
70    private boolean mIsBeingDragged;
71    private int mLastMotionY;
72    private int mActivePointerId;
73
74    private int mSidePaddings;
75    private Paint mDebugPaint;
76    private int mBackgroundRoundedRectCornerRadius;
77    private int mContentHeight;
78    private int mCollapsedSize;
79    private int mBottomStackPeekSize;
80    private int mEmptyMarginBottom;
81    private int mPaddingBetweenElements;
82
83    /**
84     * The algorithm which calculates the properties for our children
85     */
86    private StackScrollAlgorithm mStackScrollAlgorithm;
87
88    /**
89     * The current State this Layout is in
90     */
91    private final StackScrollState mCurrentStackScrollState = new StackScrollState(this);
92
93    private OnChildLocationsChangedListener mListener;
94
95    public NotificationStackScrollLayout(Context context) {
96        this(context, null);
97    }
98
99    public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
100        this(context, attrs, 0);
101    }
102
103    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
104        this(context, attrs, defStyleAttr, 0);
105    }
106
107    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
108            int defStyleRes) {
109        super(context, attrs, defStyleAttr, defStyleRes);
110        initView(context);
111        if (DEBUG) {
112            setWillNotDraw(false);
113            mDebugPaint = new Paint();
114            mDebugPaint.setColor(0xffff0000);
115            mDebugPaint.setStrokeWidth(2);
116            mDebugPaint.setStyle(Paint.Style.STROKE);
117        }
118    }
119
120    @Override
121    protected void onDraw(Canvas canvas) {
122        if (DEBUG) {
123            int y = mCollapsedSize;
124            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
125            y = (int) (getLayoutHeight() - mBottomStackPeekSize - mCollapsedSize);
126            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
127            y = (int) getLayoutHeight();
128            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
129        }
130    }
131
132    private void initView(Context context) {
133        mScroller = new OverScroller(getContext());
134        setFocusable(true);
135        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
136        final ViewConfiguration configuration = ViewConfiguration.get(context);
137        mTouchSlop = configuration.getScaledTouchSlop();
138        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
139        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
140        mOverscrollDistance = configuration.getScaledOverscrollDistance();
141        mOverflingDistance = configuration.getScaledOverflingDistance();
142        float densityScale = getResources().getDisplayMetrics().density;
143        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
144        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
145
146        mSidePaddings = context.getResources()
147                .getDimensionPixelSize(R.dimen.notification_side_padding);
148        mBackgroundRoundedRectCornerRadius = context.getResources()
149                .getDimensionPixelSize(
150                        com.android.internal.R.dimen.notification_quantum_rounded_rect_radius);
151        mCollapsedSize = context.getResources()
152                .getDimensionPixelSize(R.dimen.notification_row_min_height);
153        mBottomStackPeekSize = context.getResources()
154                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
155        mEmptyMarginBottom = context.getResources().getDimensionPixelSize(
156                R.dimen.notification_stack_margin_bottom);
157        // currently the padding is in the elements themself
158        mPaddingBetweenElements = 0;
159        mStackScrollAlgorithm = new StackScrollAlgorithm(context);
160    }
161
162    @Override
163    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
164        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
165        int mode = MeasureSpec.getMode(widthMeasureSpec);
166        int size = MeasureSpec.getSize(widthMeasureSpec);
167        int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode);
168        measureChildren(childMeasureSpec, heightMeasureSpec);
169    }
170
171    @Override
172    protected void onLayout(boolean changed, int l, int t, int r, int b) {
173
174        // we layout all our children centered on the top
175        float centerX = getWidth() / 2.0f;
176        for (int i = 0; i < getChildCount(); i++) {
177            View child = getChildAt(i);
178            float width = child.getMeasuredWidth();
179            float height = child.getMeasuredHeight();
180            int oldWidth = child.getWidth();
181            int oldHeight = child.getHeight();
182            child.layout((int) (centerX - width / 2.0f),
183                    0,
184                    (int) (centerX + width / 2.0f),
185                    (int) height);
186            updateChildOutline(child, width, height, oldWidth, oldHeight);
187        }
188        setMaxLayoutHeight(getHeight() - mEmptyMarginBottom);
189        updateScrollPositionIfNecessary();
190        updateChildren();
191        updateContentHeight();
192    }
193
194    public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
195        mListener = listener;
196    }
197
198    /**
199     * Returns the location the given child is currently rendered at.
200     *
201     * @param child the child to get the location for
202     * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants
203     */
204    public int getChildLocation(View child) {
205        ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child);
206        if (childViewState == null) {
207            return ViewState.LOCATION_UNKNOWN;
208        }
209        return childViewState.location;
210    }
211
212    private void setMaxLayoutHeight(int maxLayoutHeight) {
213        mMaxLayoutHeight = maxLayoutHeight;
214        updateAlgorithmHeight();
215    }
216
217    private void updateAlgorithmHeight() {
218        mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
219    }
220
221    /**
222     * Updates the children views according to the stack scroll algorithm. Call this whenever
223     * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
224     */
225    private void updateChildren() {
226        if (!isCurrentlyAnimating()) {
227            mCurrentStackScrollState.setScrollY(mOwnScrollY);
228            mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState);
229            mCurrentStackScrollState.apply();
230            mOwnScrollY = mCurrentStackScrollState.getScrollY();
231            if (mListener != null) {
232                mListener.onChildLocationsChanged(this);
233            }
234        } else {
235            // TODO: handle animation
236        }
237    }
238
239    private boolean isCurrentlyAnimating() {
240        return false;
241    }
242
243    private void updateChildOutline(View child,
244                                    float width,
245                                    float height,
246                                    int oldWidth,
247                                    int oldHeight) {
248        // The children currently have paddings inside themselfs because of the expansion
249        // visualization. In order for the shadows to work correctly we have to set the correct
250        // outline.
251        View container = child.findViewById(R.id.container);
252        if (container != null && (oldWidth != width || oldHeight != height)) {
253            Outline outline = getOutlineForSize(container.getLeft(),
254                    container.getTop(),
255                    container.getWidth(),
256                    container.getHeight());
257            child.setOutline(outline);
258        }
259    }
260
261    private Outline getOutlineForSize(int leftInset, int topInset, int width, int height) {
262        Outline result = new Outline();
263        result.setRoundRect(leftInset, topInset, leftInset + width, topInset + height,
264                mBackgroundRoundedRectCornerRadius);
265        return result;
266    }
267
268    private void updateScrollPositionIfNecessary() {
269        int scrollRange = getScrollRange();
270        if (scrollRange < mOwnScrollY) {
271            mOwnScrollY = scrollRange;
272        }
273    }
274
275    public void setCurrentStackHeight(int currentStackHeight) {
276        this.mCurrentStackHeight = currentStackHeight;
277        updateAlgorithmHeight();
278        updateChildren();
279    }
280
281    /**
282     * Get the current height of the view. This is at most the msize of the view given by a the
283     * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
284     *
285     * @return either the layout height or the externally defined height, whichever is smaller
286     */
287    private float getLayoutHeight() {
288        return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
289    }
290
291    public int getItemHeight() {
292        return mCollapsedSize;
293    }
294
295    public int getBottomStackPeekSize() {
296        return mBottomStackPeekSize;
297    }
298
299    public void setLongPressListener(View.OnLongClickListener listener) {
300        mSwipeHelper.setLongPressListener(listener);
301    }
302
303    public void onChildDismissed(View v) {
304        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
305        final View veto = v.findViewById(R.id.veto);
306        if (veto != null && veto.getVisibility() != View.GONE) {
307            veto.performClick();
308        }
309        setSwipingInProgress(false);
310    }
311
312    public void onBeginDrag(View v) {
313        setSwipingInProgress(true);
314    }
315
316    public void onDragCancelled(View v) {
317        setSwipingInProgress(false);
318    }
319
320    public View getChildAtPosition(MotionEvent ev) {
321        return getChildAtPosition(ev.getX(), ev.getY());
322    }
323
324    public View getChildAtRawPosition(float touchX, float touchY) {
325        int[] location = new int[2];
326        getLocationOnScreen(location);
327        return getChildAtPosition(touchX - location[0],touchY - location[1]);
328    }
329
330    public View getChildAtPosition(float touchX, float touchY) {
331        // find the view under the pointer, accounting for GONE views
332        final int count = getChildCount();
333        for (int childIdx = 0; childIdx < count; childIdx++) {
334            View slidingChild = getChildAt(childIdx);
335            if (slidingChild.getVisibility() == GONE) {
336                continue;
337            }
338            float top = slidingChild.getTranslationY();
339            float bottom = top + slidingChild.getMeasuredHeight();
340            int left = slidingChild.getLeft();
341            int right = slidingChild.getRight();
342
343            if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
344                return slidingChild;
345            }
346        }
347        return null;
348    }
349
350    public boolean canChildBeExpanded(View v) {
351        return v instanceof ExpandableNotificationRow
352                && ((ExpandableNotificationRow) v).isExpandable();
353    }
354
355    public void setUserExpandedChild(View v, boolean userExpanded) {
356        if (v instanceof ExpandableNotificationRow) {
357            ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
358        }
359    }
360
361    public void setUserLockedChild(View v, boolean userLocked) {
362        if (v instanceof ExpandableNotificationRow) {
363            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
364        }
365    }
366
367    public View getChildContentView(View v) {
368        return v;
369    }
370
371    public boolean canChildBeDismissed(View v) {
372        final View veto = v.findViewById(R.id.veto);
373        return (veto != null && veto.getVisibility() != View.GONE);
374    }
375
376    private void setSwipingInProgress(boolean isSwiped) {
377        mSwipingInProgress = isSwiped;
378        if(isSwiped) {
379            requestDisallowInterceptTouchEvent(true);
380        }
381    }
382
383    @Override
384    protected void onConfigurationChanged(Configuration newConfig) {
385        super.onConfigurationChanged(newConfig);
386        float densityScale = getResources().getDisplayMetrics().density;
387        mSwipeHelper.setDensityScale(densityScale);
388        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
389        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
390        initView(getContext());
391    }
392
393    public void dismissRowAnimated(View child, int vel) {
394        mSwipeHelper.dismissChild(child, vel);
395    }
396
397    @Override
398    public boolean onTouchEvent(MotionEvent ev) {
399        boolean scrollerWantsIt = false;
400        if (!mSwipingInProgress) {
401            scrollerWantsIt = onScrollTouch(ev);
402        }
403        boolean horizontalSwipeWantsIt = false;
404        if (!mIsBeingDragged) {
405            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
406        }
407        return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev);
408    }
409
410    private boolean onScrollTouch(MotionEvent ev) {
411        initVelocityTrackerIfNotExists();
412        mVelocityTracker.addMovement(ev);
413
414        final int action = ev.getAction();
415
416        switch (action & MotionEvent.ACTION_MASK) {
417            case MotionEvent.ACTION_DOWN: {
418                if (getChildCount() == 0) {
419                    return false;
420                }
421                boolean isBeingDragged = !mScroller.isFinished();
422                setIsBeingDragged(isBeingDragged);
423
424                /*
425                 * If being flinged and user touches, stop the fling. isFinished
426                 * will be false if being flinged.
427                 */
428                if (!mScroller.isFinished()) {
429                    mScroller.abortAnimation();
430                }
431
432                // Remember where the motion event started
433                mLastMotionY = (int) ev.getY();
434                mActivePointerId = ev.getPointerId(0);
435                break;
436            }
437            case MotionEvent.ACTION_MOVE:
438                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
439                if (activePointerIndex == -1) {
440                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
441                    break;
442                }
443
444                final int y = (int) ev.getY(activePointerIndex);
445                int deltaY = mLastMotionY - y;
446                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
447                    setIsBeingDragged(true);
448                    if (deltaY > 0) {
449                        deltaY -= mTouchSlop;
450                    } else {
451                        deltaY += mTouchSlop;
452                    }
453                }
454                if (mIsBeingDragged) {
455                    // Scroll to follow the motion event
456                    mLastMotionY = y;
457
458                    final int oldX = mScrollX;
459                    final int oldY = mOwnScrollY;
460                    final int range = getScrollRange();
461                    final int overscrollMode = getOverScrollMode();
462                    final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
463                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
464
465                    // Calling overScrollBy will call onOverScrolled, which
466                    // calls onScrollChanged if applicable.
467                    if (overScrollBy(0, deltaY, 0, mOwnScrollY,
468                            0, range, 0, mOverscrollDistance, true)) {
469                        // Break our velocity if we hit a scroll barrier.
470                        mVelocityTracker.clear();
471                    }
472                    // TODO: Overscroll
473//                    if (canOverscroll) {
474//                        final int pulledToY = oldY + deltaY;
475//                        if (pulledToY < 0) {
476//                            mEdgeGlowTop.onPull((float) deltaY / getHeight());
477//                            if (!mEdgeGlowBottom.isFinished()) {
478//                                mEdgeGlowBottom.onRelease();
479//                            }
480//                        } else if (pulledToY > range) {
481//                            mEdgeGlowBottom.onPull((float) deltaY / getHeight());
482//                            if (!mEdgeGlowTop.isFinished()) {
483//                                mEdgeGlowTop.onRelease();
484//                            }
485//                        }
486//                        if (mEdgeGlowTop != null
487//                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){
488//                            postInvalidateOnAnimation();
489//                        }
490//                    }
491                }
492                break;
493            case MotionEvent.ACTION_UP:
494                if (mIsBeingDragged) {
495                    final VelocityTracker velocityTracker = mVelocityTracker;
496                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
497                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
498
499                    if (getChildCount() > 0) {
500                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
501                            fling(-initialVelocity);
502                        } else {
503                            if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
504                                    getScrollRange())) {
505                                postInvalidateOnAnimation();
506                            }
507                        }
508                    }
509
510                    mActivePointerId = INVALID_POINTER;
511                    endDrag();
512                }
513                break;
514            case MotionEvent.ACTION_CANCEL:
515                if (mIsBeingDragged && getChildCount() > 0) {
516                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
517                        postInvalidateOnAnimation();
518                    }
519                    mActivePointerId = INVALID_POINTER;
520                    endDrag();
521                }
522                break;
523            case MotionEvent.ACTION_POINTER_DOWN: {
524                final int index = ev.getActionIndex();
525                mLastMotionY = (int) ev.getY(index);
526                mActivePointerId = ev.getPointerId(index);
527                break;
528            }
529            case MotionEvent.ACTION_POINTER_UP:
530                onSecondaryPointerUp(ev);
531                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
532                break;
533        }
534        return true;
535    }
536
537    private void onSecondaryPointerUp(MotionEvent ev) {
538        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
539                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
540        final int pointerId = ev.getPointerId(pointerIndex);
541        if (pointerId == mActivePointerId) {
542            // This was our active pointer going up. Choose a new
543            // active pointer and adjust accordingly.
544            // TODO: Make this decision more intelligent.
545            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
546            mLastMotionY = (int) ev.getY(newPointerIndex);
547            mActivePointerId = ev.getPointerId(newPointerIndex);
548            if (mVelocityTracker != null) {
549                mVelocityTracker.clear();
550            }
551        }
552    }
553
554    private void initVelocityTrackerIfNotExists() {
555        if (mVelocityTracker == null) {
556            mVelocityTracker = VelocityTracker.obtain();
557        }
558    }
559
560    private void recycleVelocityTracker() {
561        if (mVelocityTracker != null) {
562            mVelocityTracker.recycle();
563            mVelocityTracker = null;
564        }
565    }
566
567    private void initOrResetVelocityTracker() {
568        if (mVelocityTracker == null) {
569            mVelocityTracker = VelocityTracker.obtain();
570        } else {
571            mVelocityTracker.clear();
572        }
573    }
574
575    @Override
576    public void computeScroll() {
577        if (mScroller.computeScrollOffset()) {
578            // This is called at drawing time by ViewGroup.
579            int oldX = mScrollX;
580            int oldY = mOwnScrollY;
581            int x = mScroller.getCurrX();
582            int y = mScroller.getCurrY();
583
584            if (oldX != x || oldY != y) {
585                final int range = getScrollRange();
586                final int overscrollMode = getOverScrollMode();
587                final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
588                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
589
590                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
591                        0, mOverflingDistance, false);
592                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
593
594                if (canOverscroll) {
595                    // TODO: Overscroll
596//                    if (y < 0 && oldY >= 0) {
597//                        mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
598//                    } else if (y > range && oldY <= range) {
599//                        mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
600//                    }
601                }
602                updateChildren();
603            }
604
605            // Keep on drawing until the animation has finished.
606            postInvalidateOnAnimation();
607        }
608    }
609
610    public void customScrollBy(int y) {
611        mOwnScrollY += y;
612        updateChildren();
613    }
614
615    public void customScrollTo(int y) {
616        mOwnScrollY = y;
617        updateChildren();
618    }
619
620    @Override
621    protected void onOverScrolled(int scrollX, int scrollY,
622                                  boolean clampedX, boolean clampedY) {
623        // Treat animating scrolls differently; see #computeScroll() for why.
624        if (!mScroller.isFinished()) {
625            final int oldX = mScrollX;
626            final int oldY = mOwnScrollY;
627            mScrollX = scrollX;
628            mOwnScrollY = scrollY;
629            invalidateParentIfNeeded();
630            onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
631            if (clampedY) {
632                mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange());
633            }
634            updateChildren();
635        } else {
636            customScrollTo(scrollY);
637            scrollTo(scrollX, mScrollY);
638        }
639    }
640
641    private int getScrollRange() {
642        int scrollRange = 0;
643        if (getChildCount() > 0) {
644            int contentHeight = getContentHeight();
645            scrollRange = Math.max(0,
646                    contentHeight - mMaxLayoutHeight + mBottomStackPeekSize);
647        }
648        return scrollRange;
649    }
650
651    private int getContentHeight() {
652        return mContentHeight;
653    }
654
655    private void updateContentHeight() {
656        int height = 0;
657        for (int i = 0; i < getChildCount(); i++) {
658            View child = getChildAt(i);
659            height += child.getHeight();
660            if (i < getChildCount()-1) {
661                height += mPaddingBetweenElements;
662            }
663        }
664        mContentHeight = height;
665    }
666
667    /**
668     * Fling the scroll view
669     *
670     * @param velocityY The initial velocity in the Y direction. Positive
671     *                  numbers mean that the finger/cursor is moving down the screen,
672     *                  which means we want to scroll towards the top.
673     */
674    private void fling(int velocityY) {
675        if (getChildCount() > 0) {
676            int height = (int) getLayoutHeight();
677            int bottom = getContentHeight();
678
679            mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0,
680                    Math.max(0, bottom - height), 0, height/2);
681
682            postInvalidateOnAnimation();
683        }
684    }
685
686    private void endDrag() {
687        setIsBeingDragged(false);
688
689        recycleVelocityTracker();
690
691        // TODO: Overscroll
692//        if (mEdgeGlowTop != null) {
693//            mEdgeGlowTop.onRelease();
694//            mEdgeGlowBottom.onRelease();
695//        }
696    }
697
698    @Override
699    public boolean onInterceptTouchEvent(MotionEvent ev) {
700        boolean scrollWantsIt = false;
701        if (!mSwipingInProgress) {
702            scrollWantsIt = onInterceptTouchEventScroll(ev);
703        }
704        boolean swipeWantsIt = false;
705        if (!mIsBeingDragged) {
706            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
707        }
708        return swipeWantsIt || scrollWantsIt ||
709                super.onInterceptTouchEvent(ev);
710    }
711
712    @Override
713    protected void onViewRemoved(View child) {
714        super.onViewRemoved(child);
715        mCurrentStackScrollState.removeViewStateForView(child);
716    }
717
718    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
719        /*
720         * This method JUST determines whether we want to intercept the motion.
721         * If we return true, onMotionEvent will be called and we do the actual
722         * scrolling there.
723         */
724
725        /*
726        * Shortcut the most recurring case: the user is in the dragging
727        * state and he is moving his finger.  We want to intercept this
728        * motion.
729        */
730        final int action = ev.getAction();
731        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
732            return true;
733        }
734
735        /*
736         * Don't try to intercept touch if we can't scroll anyway.
737         */
738        if (mOwnScrollY == 0 && getScrollRange() == 0) {
739            return false;
740        }
741
742        switch (action & MotionEvent.ACTION_MASK) {
743            case MotionEvent.ACTION_MOVE: {
744                /*
745                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
746                 * whether the user has moved far enough from his original down touch.
747                 */
748
749                /*
750                * Locally do absolute value. mLastMotionY is set to the y value
751                * of the down event.
752                */
753                final int activePointerId = mActivePointerId;
754                if (activePointerId == INVALID_POINTER) {
755                    // If we don't have a valid id, the touch down wasn't on content.
756                    break;
757                }
758
759                final int pointerIndex = ev.findPointerIndex(activePointerId);
760                if (pointerIndex == -1) {
761                    Log.e(TAG, "Invalid pointerId=" + activePointerId
762                            + " in onInterceptTouchEvent");
763                    break;
764                }
765
766                final int y = (int) ev.getY(pointerIndex);
767                final int yDiff = Math.abs(y - mLastMotionY);
768                if (yDiff > mTouchSlop) {
769                    setIsBeingDragged(true);
770                    mLastMotionY = y;
771                    initVelocityTrackerIfNotExists();
772                    mVelocityTracker.addMovement(ev);
773                }
774                break;
775            }
776
777            case MotionEvent.ACTION_DOWN: {
778                final int y = (int) ev.getY();
779                if (getChildAtPosition(ev.getX(), y) == null) {
780                    setIsBeingDragged(false);
781                    recycleVelocityTracker();
782                    break;
783                }
784
785                /*
786                 * Remember location of down touch.
787                 * ACTION_DOWN always refers to pointer index 0.
788                 */
789                mLastMotionY = y;
790                mActivePointerId = ev.getPointerId(0);
791
792                initOrResetVelocityTracker();
793                mVelocityTracker.addMovement(ev);
794                /*
795                * If being flinged and user touches the screen, initiate drag;
796                * otherwise don't.  mScroller.isFinished should be false when
797                * being flinged.
798                */
799                boolean isBeingDragged = !mScroller.isFinished();
800                setIsBeingDragged(isBeingDragged);
801                break;
802            }
803
804            case MotionEvent.ACTION_CANCEL:
805            case MotionEvent.ACTION_UP:
806                /* Release the drag */
807                setIsBeingDragged(false);
808                mActivePointerId = INVALID_POINTER;
809                recycleVelocityTracker();
810                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
811                    postInvalidateOnAnimation();
812                }
813                break;
814            case MotionEvent.ACTION_POINTER_UP:
815                onSecondaryPointerUp(ev);
816                break;
817        }
818
819        /*
820        * The only time we want to intercept motion events is if we are in the
821        * drag mode.
822        */
823        return mIsBeingDragged;
824    }
825
826    private void setIsBeingDragged(boolean isDragged) {
827        mIsBeingDragged = isDragged;
828        if (isDragged) {
829            requestDisallowInterceptTouchEvent(true);
830            mSwipeHelper.removeLongPressCallback();
831        }
832    }
833
834    @Override
835    public void onWindowFocusChanged(boolean hasWindowFocus) {
836        super.onWindowFocusChanged(hasWindowFocus);
837        if (!hasWindowFocus) {
838            mSwipeHelper.removeLongPressCallback();
839        }
840    }
841
842    @Override
843    public boolean isScrolledToTop() {
844        return mOwnScrollY == 0;
845    }
846
847    @Override
848    public boolean isScrolledToBottom() {
849        return mOwnScrollY >= getScrollRange();
850    }
851
852    @Override
853    public View getHostView() {
854        return this;
855    }
856
857    public int getEmptyBottomMargin() {
858        return Math.max(getHeight() - mContentHeight, 0);
859    }
860
861    /**
862     * A listener that is notified when some child locations might have changed.
863     */
864    public interface OnChildLocationsChangedListener {
865        public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
866    }
867}
868