NotificationStackScrollLayout.java revision 0dd6881ea481c855976214807c17595b34a2920a
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.systemui.statusbar.stack;
18
19import android.content.Context;
20import android.content.res.Configuration;
21
22import android.graphics.Canvas;
23import android.graphics.Paint;
24
25import android.util.AttributeSet;
26import android.util.Log;
27
28import android.view.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31import android.view.ViewConfiguration;
32import android.view.ViewGroup;
33import android.view.ViewTreeObserver;
34import android.view.animation.AnimationUtils;
35import android.widget.OverScroller;
36
37import com.android.systemui.ExpandHelper;
38import com.android.systemui.R;
39import com.android.systemui.SwipeHelper;
40import com.android.systemui.statusbar.ExpandableNotificationRow;
41import com.android.systemui.statusbar.ExpandableView;
42import com.android.systemui.statusbar.stack.StackScrollState.ViewState;
43import com.android.systemui.statusbar.policy.ScrollAdapter;
44
45import java.util.ArrayList;
46
47/**
48 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
49 */
50public class NotificationStackScrollLayout extends ViewGroup
51        implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
52        ExpandableView.OnHeightChangedListener {
53
54    private static final String TAG = "NotificationStackScrollLayout";
55    private static final boolean DEBUG = false;
56
57    /**
58     * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
59     */
60    private static final int INVALID_POINTER = -1;
61
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 mOverscrollDistance;
74    private int mOverflingDistance;
75    private boolean mIsBeingDragged;
76    private int mLastMotionY;
77    private int mActivePointerId;
78
79    private int mSidePaddings;
80    private Paint mDebugPaint;
81    private int mContentHeight;
82    private int mCollapsedSize;
83    private int mBottomStackPeekSize;
84    private int mEmptyMarginBottom;
85    private int mPaddingBetweenElements;
86    private int mTopPadding;
87    private boolean mListenForHeightChanges = true;
88
89    /**
90     * The algorithm which calculates the properties for our children
91     */
92    private StackScrollAlgorithm mStackScrollAlgorithm;
93
94    /**
95     * The current State this Layout is in
96     */
97    private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
98    private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>();
99    private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
100    private ArrayList<AnimationEvent> mAnimationEvents
101            = new ArrayList<AnimationEvent>();
102    private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
103    private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
104
105    private OnChildLocationsChangedListener mListener;
106    private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
107    private boolean mNeedsAnimation;
108    private boolean mTopPaddingNeedsAnimation;
109    private boolean mIsExpanded = true;
110    private boolean mChildrenUpdateRequested;
111    private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
112            = new ViewTreeObserver.OnPreDrawListener() {
113        @Override
114        public boolean onPreDraw() {
115            updateChildren();
116            mChildrenUpdateRequested = false;
117            getViewTreeObserver().removeOnPreDrawListener(this);
118            return true;
119        }
120    };
121
122    public NotificationStackScrollLayout(Context context) {
123        this(context, null);
124    }
125
126    public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
127        this(context, attrs, 0);
128    }
129
130    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
131        this(context, attrs, defStyleAttr, 0);
132    }
133
134    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
135            int defStyleRes) {
136        super(context, attrs, defStyleAttr, defStyleRes);
137        initView(context);
138        if (DEBUG) {
139            setWillNotDraw(false);
140            mDebugPaint = new Paint();
141            mDebugPaint.setColor(0xffff0000);
142            mDebugPaint.setStrokeWidth(2);
143            mDebugPaint.setStyle(Paint.Style.STROKE);
144        }
145    }
146
147    @Override
148    protected void onDraw(Canvas canvas) {
149        if (DEBUG) {
150            int y = mCollapsedSize;
151            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
152            y = (int) (getLayoutHeight() - mBottomStackPeekSize - mCollapsedSize);
153            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
154            y = (int) getLayoutHeight();
155            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
156        }
157    }
158
159    private void initView(Context context) {
160        mScroller = new OverScroller(getContext());
161        setFocusable(true);
162        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
163        setClipChildren(false);
164        final ViewConfiguration configuration = ViewConfiguration.get(context);
165        mTouchSlop = configuration.getScaledTouchSlop();
166        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
167        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
168        mOverscrollDistance = configuration.getScaledOverscrollDistance();
169        mOverflingDistance = configuration.getScaledOverflingDistance();
170        float densityScale = getResources().getDisplayMetrics().density;
171        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
172        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
173
174        mSidePaddings = context.getResources()
175                .getDimensionPixelSize(R.dimen.notification_side_padding);
176        mCollapsedSize = context.getResources()
177                .getDimensionPixelSize(R.dimen.notification_min_height);
178        mBottomStackPeekSize = context.getResources()
179                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
180        mEmptyMarginBottom = context.getResources().getDimensionPixelSize(
181                R.dimen.notification_stack_margin_bottom);
182        mPaddingBetweenElements = context.getResources()
183                .getDimensionPixelSize(R.dimen.notification_padding);
184        mStackScrollAlgorithm = new StackScrollAlgorithm(context);
185    }
186
187    @Override
188    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
189        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
190        int mode = MeasureSpec.getMode(widthMeasureSpec);
191        int size = MeasureSpec.getSize(widthMeasureSpec);
192        int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode);
193        measureChildren(childMeasureSpec, heightMeasureSpec);
194    }
195
196    @Override
197    protected void onLayout(boolean changed, int l, int t, int r, int b) {
198
199        // we layout all our children centered on the top
200        float centerX = getWidth() / 2.0f;
201        for (int i = 0; i < getChildCount(); i++) {
202            View child = getChildAt(i);
203            float width = child.getMeasuredWidth();
204            float height = child.getMeasuredHeight();
205            child.layout((int) (centerX - width / 2.0f),
206                    0,
207                    (int) (centerX + width / 2.0f),
208                    (int) height);
209        }
210        setMaxLayoutHeight(getHeight() - mEmptyMarginBottom);
211        updateContentHeight();
212        updateScrollPositionIfNecessary();
213        requestChildrenUpdate();
214    }
215
216    public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
217        mListener = listener;
218    }
219
220    /**
221     * Returns the location the given child is currently rendered at.
222     *
223     * @param child the child to get the location for
224     * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants
225     */
226    public int getChildLocation(View child) {
227        ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child);
228        if (childViewState == null) {
229            return ViewState.LOCATION_UNKNOWN;
230        }
231        return childViewState.location;
232    }
233
234    private void setMaxLayoutHeight(int maxLayoutHeight) {
235        mMaxLayoutHeight = maxLayoutHeight;
236        updateAlgorithmHeightAndPadding();
237    }
238
239    private void updateAlgorithmHeightAndPadding() {
240        mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
241        mStackScrollAlgorithm.setTopPadding(mTopPadding);
242    }
243
244    /**
245     * @return whether the height of the layout needs to be adapted, in order to ensure that the
246     *         last child is not in the bottom stack.
247     */
248    private boolean needsHeightAdaption() {
249        View lastChild = getLastChildNotGone();
250        View firstChild = getFirstChildNotGone();
251        boolean isLastChildExpanded = isViewExpanded(lastChild);
252        return isLastChildExpanded && lastChild != firstChild;
253    }
254
255    private boolean isViewExpanded(View view) {
256        if (view != null) {
257            ExpandableView expandView = (ExpandableView) view;
258            return expandView.getActualHeight() > mCollapsedSize;
259        }
260        return false;
261    }
262
263    /**
264     * Updates the children views according to the stack scroll algorithm. Call this whenever
265     * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
266     */
267    private void updateChildren() {
268        mCurrentStackScrollState.setScrollY(mOwnScrollY);
269        mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState);
270        if (!isCurrentlyAnimating() && !mNeedsAnimation) {
271            applyCurrentState();
272        } else {
273            startAnimationToState();
274        }
275    }
276
277    private void requestChildrenUpdate() {
278        if (!mChildrenUpdateRequested) {
279            getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
280            mChildrenUpdateRequested = true;
281            invalidate();
282        }
283    }
284
285    private boolean isCurrentlyAnimating() {
286        return mStateAnimator.isRunning();
287    }
288
289    private void updateScrollPositionIfNecessary() {
290        int scrollRange = getScrollRange();
291        if (scrollRange < mOwnScrollY) {
292            mOwnScrollY = scrollRange;
293        }
294    }
295
296    public int getTopPadding() {
297        return mTopPadding;
298    }
299
300    public void setTopPadding(int topPadding, boolean animate) {
301        if (mTopPadding != topPadding) {
302            mTopPadding = topPadding;
303            updateAlgorithmHeightAndPadding();
304            updateContentHeight();
305            if (animate) {
306                mTopPaddingNeedsAnimation = true;
307                mNeedsAnimation =  true;
308            }
309            requestChildrenUpdate();
310        }
311    }
312
313    /**
314     * Update the height of the stack to a new height.
315     *
316     * @param height the new height of the stack
317     */
318    public void setStackHeight(float height) {
319        setIsExpanded(height > 0.0f);
320        int newStackHeight = (int) height;
321        int itemHeight = getItemHeight();
322        int bottomStackPeekSize = mBottomStackPeekSize;
323        int minStackHeight = itemHeight + bottomStackPeekSize;
324        int stackHeight;
325        if (newStackHeight - mTopPadding >= minStackHeight) {
326            setTranslationY(0);
327            stackHeight = newStackHeight;
328        } else {
329
330            // We did not reach the position yet where we actually start growing,
331            // so we translate the stack upwards.
332            int translationY = (newStackHeight - minStackHeight);
333            // A slight parallax effect is introduced in order for the stack to catch up with
334            // the top card.
335            float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight;
336            partiallyThere = Math.max(0, partiallyThere);
337            translationY += (1 - partiallyThere) * bottomStackPeekSize;
338            setTranslationY(translationY - mTopPadding);
339            stackHeight = (int) (height - (translationY - mTopPadding));
340        }
341        if (stackHeight != mCurrentStackHeight) {
342            mCurrentStackHeight = stackHeight;
343            updateAlgorithmHeightAndPadding();
344            requestChildrenUpdate();
345        }
346    }
347
348    /**
349     * Get the current height of the view. This is at most the msize of the view given by a the
350     * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
351     *
352     * @return either the layout height or the externally defined height, whichever is smaller
353     */
354    private int getLayoutHeight() {
355        return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
356    }
357
358    public int getItemHeight() {
359        return mCollapsedSize;
360    }
361
362    public int getBottomStackPeekSize() {
363        return mBottomStackPeekSize;
364    }
365
366    public void setLongPressListener(View.OnLongClickListener listener) {
367        mSwipeHelper.setLongPressListener(listener);
368    }
369
370    public void onChildDismissed(View v) {
371        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
372        final View veto = v.findViewById(R.id.veto);
373        if (veto != null && veto.getVisibility() != View.GONE) {
374            veto.performClick();
375        }
376        setSwipingInProgress(false);
377        mSwipedOutViews.add(v);
378    }
379
380    public void onBeginDrag(View v) {
381        setSwipingInProgress(true);
382    }
383
384    public void onDragCancelled(View v) {
385        setSwipingInProgress(false);
386    }
387
388    public View getChildAtPosition(MotionEvent ev) {
389        return getChildAtPosition(ev.getX(), ev.getY());
390    }
391
392    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
393        int[] location = new int[2];
394        getLocationOnScreen(location);
395        return getChildAtPosition(touchX - location[0], touchY - location[1]);
396    }
397
398    public ExpandableView getChildAtPosition(float touchX, float touchY) {
399        // find the view under the pointer, accounting for GONE views
400        final int count = getChildCount();
401        for (int childIdx = 0; childIdx < count; childIdx++) {
402            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
403            if (slidingChild.getVisibility() == GONE) {
404                continue;
405            }
406            float top = slidingChild.getTranslationY();
407            float bottom = top + slidingChild.getActualHeight();
408            int left = slidingChild.getLeft();
409            int right = slidingChild.getRight();
410
411            if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
412                return slidingChild;
413            }
414        }
415        return null;
416    }
417
418    public boolean canChildBeExpanded(View v) {
419        return v instanceof ExpandableNotificationRow
420                && ((ExpandableNotificationRow) v).isExpandable();
421    }
422
423    public void setUserExpandedChild(View v, boolean userExpanded) {
424        if (v instanceof ExpandableNotificationRow) {
425            ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
426        }
427    }
428
429    public void setUserLockedChild(View v, boolean userLocked) {
430        if (v instanceof ExpandableNotificationRow) {
431            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
432        }
433    }
434
435    public View getChildContentView(View v) {
436        return v;
437    }
438
439    public boolean canChildBeDismissed(View v) {
440        final View veto = v.findViewById(R.id.veto);
441        return (veto != null && veto.getVisibility() != View.GONE);
442    }
443
444    private void setSwipingInProgress(boolean isSwiped) {
445        mSwipingInProgress = isSwiped;
446        if(isSwiped) {
447            requestDisallowInterceptTouchEvent(true);
448        }
449    }
450
451    @Override
452    protected void onConfigurationChanged(Configuration newConfig) {
453        super.onConfigurationChanged(newConfig);
454        float densityScale = getResources().getDisplayMetrics().density;
455        mSwipeHelper.setDensityScale(densityScale);
456        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
457        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
458        initView(getContext());
459    }
460
461    public void dismissRowAnimated(View child, int vel) {
462        mSwipeHelper.dismissChild(child, vel);
463    }
464
465    @Override
466    public boolean onTouchEvent(MotionEvent ev) {
467        boolean scrollerWantsIt = false;
468        if (!mSwipingInProgress) {
469            scrollerWantsIt = onScrollTouch(ev);
470        }
471        boolean horizontalSwipeWantsIt = false;
472        if (!mIsBeingDragged) {
473            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
474        }
475        return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev);
476    }
477
478    private boolean onScrollTouch(MotionEvent ev) {
479        initVelocityTrackerIfNotExists();
480        mVelocityTracker.addMovement(ev);
481
482        final int action = ev.getAction();
483
484        switch (action & MotionEvent.ACTION_MASK) {
485            case MotionEvent.ACTION_DOWN: {
486                if (getChildCount() == 0) {
487                    return false;
488                }
489                boolean isBeingDragged = !mScroller.isFinished();
490                setIsBeingDragged(isBeingDragged);
491
492                /*
493                 * If being flinged and user touches, stop the fling. isFinished
494                 * will be false if being flinged.
495                 */
496                if (!mScroller.isFinished()) {
497                    mScroller.abortAnimation();
498                }
499
500                // Remember where the motion event started
501                mLastMotionY = (int) ev.getY();
502                mActivePointerId = ev.getPointerId(0);
503                break;
504            }
505            case MotionEvent.ACTION_MOVE:
506                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
507                if (activePointerIndex == -1) {
508                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
509                    break;
510                }
511
512                final int y = (int) ev.getY(activePointerIndex);
513                int deltaY = mLastMotionY - y;
514                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
515                    setIsBeingDragged(true);
516                    if (deltaY > 0) {
517                        deltaY -= mTouchSlop;
518                    } else {
519                        deltaY += mTouchSlop;
520                    }
521                }
522                if (mIsBeingDragged) {
523                    // Scroll to follow the motion event
524                    mLastMotionY = y;
525
526                    final int oldX = mScrollX;
527                    final int oldY = mOwnScrollY;
528                    final int range = getScrollRange();
529                    final int overscrollMode = getOverScrollMode();
530                    final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
531                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
532
533                    // Calling overScrollBy will call onOverScrolled, which
534                    // calls onScrollChanged if applicable.
535                    if (overScrollBy(0, deltaY, 0, mOwnScrollY,
536                            0, range, 0, mOverscrollDistance, true)) {
537                        // Break our velocity if we hit a scroll barrier.
538                        mVelocityTracker.clear();
539                    }
540                    // TODO: Overscroll
541//                    if (canOverscroll) {
542//                        final int pulledToY = oldY + deltaY;
543//                        if (pulledToY < 0) {
544//                            mEdgeGlowTop.onPull((float) deltaY / getHeight());
545//                            if (!mEdgeGlowBottom.isFinished()) {
546//                                mEdgeGlowBottom.onRelease();
547//                            }
548//                        } else if (pulledToY > range) {
549//                            mEdgeGlowBottom.onPull((float) deltaY / getHeight());
550//                            if (!mEdgeGlowTop.isFinished()) {
551//                                mEdgeGlowTop.onRelease();
552//                            }
553//                        }
554//                        if (mEdgeGlowTop != null
555//                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){
556//                            postInvalidateOnAnimation();
557//                        }
558//                    }
559                }
560                break;
561            case MotionEvent.ACTION_UP:
562                if (mIsBeingDragged) {
563                    final VelocityTracker velocityTracker = mVelocityTracker;
564                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
565                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
566
567                    if (getChildCount() > 0) {
568                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
569                            fling(-initialVelocity);
570                        } else {
571                            if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
572                                    getScrollRange())) {
573                                postInvalidateOnAnimation();
574                            }
575                        }
576                    }
577
578                    mActivePointerId = INVALID_POINTER;
579                    endDrag();
580                }
581                break;
582            case MotionEvent.ACTION_CANCEL:
583                if (mIsBeingDragged && getChildCount() > 0) {
584                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
585                        postInvalidateOnAnimation();
586                    }
587                    mActivePointerId = INVALID_POINTER;
588                    endDrag();
589                }
590                break;
591            case MotionEvent.ACTION_POINTER_DOWN: {
592                final int index = ev.getActionIndex();
593                mLastMotionY = (int) ev.getY(index);
594                mActivePointerId = ev.getPointerId(index);
595                break;
596            }
597            case MotionEvent.ACTION_POINTER_UP:
598                onSecondaryPointerUp(ev);
599                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
600                break;
601        }
602        return true;
603    }
604
605    private void onSecondaryPointerUp(MotionEvent ev) {
606        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
607                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
608        final int pointerId = ev.getPointerId(pointerIndex);
609        if (pointerId == mActivePointerId) {
610            // This was our active pointer going up. Choose a new
611            // active pointer and adjust accordingly.
612            // TODO: Make this decision more intelligent.
613            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
614            mLastMotionY = (int) ev.getY(newPointerIndex);
615            mActivePointerId = ev.getPointerId(newPointerIndex);
616            if (mVelocityTracker != null) {
617                mVelocityTracker.clear();
618            }
619        }
620    }
621
622    private void initVelocityTrackerIfNotExists() {
623        if (mVelocityTracker == null) {
624            mVelocityTracker = VelocityTracker.obtain();
625        }
626    }
627
628    private void recycleVelocityTracker() {
629        if (mVelocityTracker != null) {
630            mVelocityTracker.recycle();
631            mVelocityTracker = null;
632        }
633    }
634
635    private void initOrResetVelocityTracker() {
636        if (mVelocityTracker == null) {
637            mVelocityTracker = VelocityTracker.obtain();
638        } else {
639            mVelocityTracker.clear();
640        }
641    }
642
643    @Override
644    public void computeScroll() {
645        if (mScroller.computeScrollOffset()) {
646            // This is called at drawing time by ViewGroup.
647            int oldX = mScrollX;
648            int oldY = mOwnScrollY;
649            int x = mScroller.getCurrX();
650            int y = mScroller.getCurrY();
651
652            if (oldX != x || oldY != y) {
653                final int range = getScrollRange();
654                final int overscrollMode = getOverScrollMode();
655                final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
656                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
657
658                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
659                        0, mOverflingDistance, false);
660                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
661
662                if (canOverscroll) {
663                    // TODO: Overscroll
664//                    if (y < 0 && oldY >= 0) {
665//                        mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
666//                    } else if (y > range && oldY <= range) {
667//                        mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
668//                    }
669                }
670                updateChildren();
671            }
672
673            // Keep on drawing until the animation has finished.
674            postInvalidateOnAnimation();
675        }
676    }
677
678    private void customScrollTo(int y) {
679        mOwnScrollY = y;
680        updateChildren();
681    }
682
683    @Override
684    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
685        // Treat animating scrolls differently; see #computeScroll() for why.
686        if (!mScroller.isFinished()) {
687            final int oldX = mScrollX;
688            final int oldY = mOwnScrollY;
689            mScrollX = scrollX;
690            mOwnScrollY = scrollY;
691            invalidateParentIfNeeded();
692            onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
693            if (clampedY) {
694                mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange());
695            }
696            updateChildren();
697        } else {
698            customScrollTo(scrollY);
699            scrollTo(scrollX, mScrollY);
700        }
701    }
702
703    private int getScrollRange() {
704        int scrollRange = 0;
705        ExpandableView firstChild = (ExpandableView) getFirstChildNotGone();
706        if (firstChild != null) {
707            int contentHeight = getContentHeight();
708            int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild);
709
710            scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize);
711            if (scrollRange > 0) {
712                View lastChild = getLastChildNotGone();
713                if (isViewExpanded(lastChild)) {
714                    // last child is expanded, so we have to ensure that it can exit the
715                    // bottom stack
716                    scrollRange += mCollapsedSize + mPaddingBetweenElements;
717                }
718                // We want to at least be able collapse the first item and not ending in a weird
719                // end state.
720                scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize);
721            }
722        }
723        return scrollRange;
724    }
725
726    /**
727     * @return the first child which has visibility unequal to GONE
728     */
729    private View getFirstChildNotGone() {
730        int childCount = getChildCount();
731        for (int i = 0; i < childCount; i++) {
732            View child = getChildAt(i);
733            if (child.getVisibility() != View.GONE) {
734                return child;
735            }
736        }
737        return null;
738    }
739
740    /**
741     * @return the last child which has visibility unequal to GONE
742     */
743    private View getLastChildNotGone() {
744        int childCount = getChildCount();
745        for (int i = childCount - 1; i >= 0; i--) {
746            View child = getChildAt(i);
747            if (child.getVisibility() != View.GONE) {
748                return child;
749            }
750        }
751        return null;
752    }
753
754    private int getMaxExpandHeight(View view) {
755        if (view instanceof ExpandableNotificationRow) {
756            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
757            return row.getMaximumAllowedExpandHeight();
758        }
759        return view.getHeight();
760    }
761
762    private int getContentHeight() {
763        return mContentHeight;
764    }
765
766    private void updateContentHeight() {
767        int height = 0;
768        for (int i = 0; i < getChildCount(); i++) {
769            View child = getChildAt(i);
770            if (child.getVisibility() != View.GONE) {
771                if (height != 0) {
772                    // add the padding before this element
773                    height += mPaddingBetweenElements;
774                }
775                if (child instanceof ExpandableNotificationRow) {
776                    ExpandableNotificationRow row = (ExpandableNotificationRow) child;
777                    height += row.getMaximumAllowedExpandHeight();
778                } else if (child instanceof ExpandableView) {
779                    ExpandableView expandableView = (ExpandableView) child;
780                    height += expandableView.getActualHeight();
781                }
782            }
783        }
784        mContentHeight = height + mTopPadding;
785    }
786
787    /**
788     * Fling the scroll view
789     *
790     * @param velocityY The initial velocity in the Y direction. Positive
791     *                  numbers mean that the finger/cursor is moving down the screen,
792     *                  which means we want to scroll towards the top.
793     */
794    private void fling(int velocityY) {
795        if (getChildCount() > 0) {
796            int height = (int) getLayoutHeight();
797            int bottom = getContentHeight();
798
799            mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0,
800                    Math.max(0, bottom - height), 0, height/2);
801
802            postInvalidateOnAnimation();
803        }
804    }
805
806    private void endDrag() {
807        setIsBeingDragged(false);
808
809        recycleVelocityTracker();
810
811        // TODO: Overscroll
812//        if (mEdgeGlowTop != null) {
813//            mEdgeGlowTop.onRelease();
814//            mEdgeGlowBottom.onRelease();
815//        }
816    }
817
818    @Override
819    public boolean onInterceptTouchEvent(MotionEvent ev) {
820        boolean scrollWantsIt = false;
821        if (!mSwipingInProgress) {
822            scrollWantsIt = onInterceptTouchEventScroll(ev);
823        }
824        boolean swipeWantsIt = false;
825        if (!mIsBeingDragged) {
826            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
827        }
828        return swipeWantsIt || scrollWantsIt ||
829                super.onInterceptTouchEvent(ev);
830    }
831
832    @Override
833    protected void onViewRemoved(View child) {
834        super.onViewRemoved(child);
835        ((ExpandableView) child).setOnHeightChangedListener(null);
836        mCurrentStackScrollState.removeViewStateForView(child);
837        mStackScrollAlgorithm.notifyChildrenChanged(this);
838        updateScrollStateForRemovedChild(child);
839        if (mIsExpanded) {
840
841            if (!mChildrenToAddAnimated.contains(child)) {
842                // Generate Animations
843                mChildrenToRemoveAnimated.add(child);
844                mNeedsAnimation = true;
845            } else {
846                mChildrenToAddAnimated.remove(child);
847            }
848        }
849    }
850
851    /**
852     * Updates the scroll position when a child was removed
853     *
854     * @param removedChild the removed child
855     */
856    private void updateScrollStateForRemovedChild(View removedChild) {
857        int startingPosition = getPositionInLinearLayout(removedChild);
858        int childHeight = removedChild.getHeight() + mPaddingBetweenElements;
859        int endPosition = startingPosition + childHeight;
860        if (endPosition <= mOwnScrollY) {
861            // This child is fully scrolled of the top, so we have to deduct its height from the
862            // scrollPosition
863            mOwnScrollY -= childHeight;
864        } else if (startingPosition < mOwnScrollY) {
865            // This child is currently being scrolled into, set the scroll position to the start of
866            // this child
867            mOwnScrollY = startingPosition;
868        }
869    }
870
871    private int getPositionInLinearLayout(View requestedChild) {
872        int position = 0;
873        for (int i = 0; i < getChildCount(); i++) {
874            View child = getChildAt(i);
875            if (child == requestedChild) {
876                return position;
877            }
878            if (child.getVisibility() != View.GONE) {
879                position += child.getHeight();
880                if (i < getChildCount()-1) {
881                    position += mPaddingBetweenElements;
882                }
883            }
884        }
885        return 0;
886    }
887
888    @Override
889    protected void onViewAdded(View child) {
890        super.onViewAdded(child);
891        mStackScrollAlgorithm.notifyChildrenChanged(this);
892        ((ExpandableView) child).setOnHeightChangedListener(this);
893        if (child.getVisibility() != View.GONE) {
894            generateAddAnimation(child);
895        }
896    }
897
898    public void generateAddAnimation(View child) {
899        if (mIsExpanded) {
900
901            // Generate Animations
902            mChildrenToAddAnimated.add(child);
903            mNeedsAnimation = true;
904        }
905    }
906
907    /**
908     * Change the position of child to a new location
909     *
910     * @param child the view to change the position for
911     * @param newIndex the new index
912     */
913    public void changeViewPosition(View child, int newIndex) {
914        if (child != null && child.getParent() == this) {
915            // TODO: handle this
916        }
917    }
918
919    private void startAnimationToState() {
920        if (mNeedsAnimation) {
921            generateChildHierarchyEvents();
922            mNeedsAnimation = false;
923        }
924        if (!mAnimationEvents.isEmpty()) {
925            mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState);
926        } else {
927            applyCurrentState();
928        }
929    }
930
931    private void generateChildHierarchyEvents() {
932        generateChildAdditionEvents();
933        generateChildRemovalEvents();
934        generateTopPaddingEvent();
935        mNeedsAnimation = false;
936    }
937
938    private void generateChildRemovalEvents() {
939        for (View  child : mChildrenToRemoveAnimated) {
940            boolean childWasSwipedOut = mSwipedOutViews.contains(child);
941            int animationType = childWasSwipedOut
942                    ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
943                    : AnimationEvent.ANIMATION_TYPE_REMOVE;
944            mAnimationEvents.add(new AnimationEvent(child, animationType));
945        }
946        mSwipedOutViews.clear();
947        mChildrenToRemoveAnimated.clear();
948    }
949
950    private void generateChildAdditionEvents() {
951        for (View  child : mChildrenToAddAnimated) {
952            mAnimationEvents.add(new AnimationEvent(child,
953                    AnimationEvent.ANIMATION_TYPE_ADD));
954        }
955        mChildrenToAddAnimated.clear();
956    }
957
958    private void generateTopPaddingEvent() {
959        mAnimationEvents.add(
960                new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED));
961        mTopPaddingNeedsAnimation = false;
962    }
963
964    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
965        /*
966         * This method JUST determines whether we want to intercept the motion.
967         * If we return true, onMotionEvent will be called and we do the actual
968         * scrolling there.
969         */
970
971        /*
972        * Shortcut the most recurring case: the user is in the dragging
973        * state and he is moving his finger.  We want to intercept this
974        * motion.
975        */
976        final int action = ev.getAction();
977        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
978            return true;
979        }
980
981        /*
982         * Don't try to intercept touch if we can't scroll anyway.
983         */
984        if (mOwnScrollY == 0 && getScrollRange() == 0) {
985            return false;
986        }
987
988        switch (action & MotionEvent.ACTION_MASK) {
989            case MotionEvent.ACTION_MOVE: {
990                /*
991                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
992                 * whether the user has moved far enough from his original down touch.
993                 */
994
995                /*
996                * Locally do absolute value. mLastMotionY is set to the y value
997                * of the down event.
998                */
999                final int activePointerId = mActivePointerId;
1000                if (activePointerId == INVALID_POINTER) {
1001                    // If we don't have a valid id, the touch down wasn't on content.
1002                    break;
1003                }
1004
1005                final int pointerIndex = ev.findPointerIndex(activePointerId);
1006                if (pointerIndex == -1) {
1007                    Log.e(TAG, "Invalid pointerId=" + activePointerId
1008                            + " in onInterceptTouchEvent");
1009                    break;
1010                }
1011
1012                final int y = (int) ev.getY(pointerIndex);
1013                final int yDiff = Math.abs(y - mLastMotionY);
1014                if (yDiff > mTouchSlop) {
1015                    setIsBeingDragged(true);
1016                    mLastMotionY = y;
1017                    initVelocityTrackerIfNotExists();
1018                    mVelocityTracker.addMovement(ev);
1019                }
1020                break;
1021            }
1022
1023            case MotionEvent.ACTION_DOWN: {
1024                final int y = (int) ev.getY();
1025                if (getChildAtPosition(ev.getX(), y) == null) {
1026                    setIsBeingDragged(false);
1027                    recycleVelocityTracker();
1028                    break;
1029                }
1030
1031                /*
1032                 * Remember location of down touch.
1033                 * ACTION_DOWN always refers to pointer index 0.
1034                 */
1035                mLastMotionY = y;
1036                mActivePointerId = ev.getPointerId(0);
1037
1038                initOrResetVelocityTracker();
1039                mVelocityTracker.addMovement(ev);
1040                /*
1041                * If being flinged and user touches the screen, initiate drag;
1042                * otherwise don't.  mScroller.isFinished should be false when
1043                * being flinged.
1044                */
1045                boolean isBeingDragged = !mScroller.isFinished();
1046                setIsBeingDragged(isBeingDragged);
1047                break;
1048            }
1049
1050            case MotionEvent.ACTION_CANCEL:
1051            case MotionEvent.ACTION_UP:
1052                /* Release the drag */
1053                setIsBeingDragged(false);
1054                mActivePointerId = INVALID_POINTER;
1055                recycleVelocityTracker();
1056                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
1057                    postInvalidateOnAnimation();
1058                }
1059                break;
1060            case MotionEvent.ACTION_POINTER_UP:
1061                onSecondaryPointerUp(ev);
1062                break;
1063        }
1064
1065        /*
1066        * The only time we want to intercept motion events is if we are in the
1067        * drag mode.
1068        */
1069        return mIsBeingDragged;
1070    }
1071
1072    private void setIsBeingDragged(boolean isDragged) {
1073        mIsBeingDragged = isDragged;
1074        if (isDragged) {
1075            requestDisallowInterceptTouchEvent(true);
1076            mSwipeHelper.removeLongPressCallback();
1077        }
1078    }
1079
1080    @Override
1081    public void onWindowFocusChanged(boolean hasWindowFocus) {
1082        super.onWindowFocusChanged(hasWindowFocus);
1083        if (!hasWindowFocus) {
1084            mSwipeHelper.removeLongPressCallback();
1085        }
1086    }
1087
1088    @Override
1089    public boolean isScrolledToTop() {
1090        return mOwnScrollY == 0;
1091    }
1092
1093    @Override
1094    public boolean isScrolledToBottom() {
1095        return mOwnScrollY >= getScrollRange();
1096    }
1097
1098    @Override
1099    public View getHostView() {
1100        return this;
1101    }
1102
1103    public int getEmptyBottomMargin() {
1104        int emptyMargin = mMaxLayoutHeight - mContentHeight;
1105        if (needsHeightAdaption()) {
1106            emptyMargin = emptyMargin - mCollapsedSize - mBottomStackPeekSize;
1107        }
1108        return Math.max(emptyMargin, 0);
1109    }
1110
1111    public void onExpansionStarted() {
1112        mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState);
1113    }
1114
1115    public void onExpansionStopped() {
1116        mStackScrollAlgorithm.onExpansionStopped();
1117    }
1118
1119    private void setIsExpanded(boolean isExpanded) {
1120        mIsExpanded = isExpanded;
1121        mStackScrollAlgorithm.setIsExpanded(isExpanded);
1122        if (!isExpanded) {
1123            mOwnScrollY = 0;
1124        }
1125    }
1126
1127    @Override
1128    public void onHeightChanged(ExpandableView view) {
1129        if (mListenForHeightChanges && !isCurrentlyAnimating()) {
1130            updateContentHeight();
1131            updateScrollPositionIfNecessary();
1132            if (mOnHeightChangedListener != null) {
1133                mOnHeightChangedListener.onHeightChanged(view);
1134            }
1135            requestChildrenUpdate();
1136        }
1137    }
1138
1139    public void setOnHeightChangedListener(
1140            ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
1141        this.mOnHeightChangedListener = mOnHeightChangedListener;
1142    }
1143
1144    public void onChildAnimationFinished() {
1145        requestChildrenUpdate();
1146        mAnimationEvents.clear();
1147    }
1148
1149    private void applyCurrentState() {
1150        mListenForHeightChanges = false;
1151        mCurrentStackScrollState.apply();
1152        mListenForHeightChanges = true;
1153        if (mListener != null) {
1154            mListener.onChildLocationsChanged(this);
1155        }
1156    }
1157
1158    /**
1159     * A listener that is notified when some child locations might have changed.
1160     */
1161    public interface OnChildLocationsChangedListener {
1162        public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
1163    }
1164
1165    static class AnimationEvent {
1166
1167        static int ANIMATION_TYPE_ADD = 1;
1168        static int ANIMATION_TYPE_REMOVE = 2;
1169        static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 3;
1170        static int ANIMATION_TYPE_TOP_PADDING_CHANGED = 4;
1171
1172        final long eventStartTime;
1173        final View changingView;
1174        final int animationType;
1175
1176        AnimationEvent(View view, int type) {
1177            eventStartTime = AnimationUtils.currentAnimationTimeMillis();
1178            changingView = view;
1179            animationType = type;
1180        }
1181    }
1182
1183}
1184