NotificationStackScrollLayout.java revision 98fb09c2b2dbf57803a8737ee7b73cf167721312
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            if (mOnHeightChangedListener != null) {
311                mOnHeightChangedListener.onHeightChanged(null);
312            }
313        }
314    }
315
316    /**
317     * Update the height of the stack to a new height.
318     *
319     * @param height the new height of the stack
320     */
321    public void setStackHeight(float height) {
322        setIsExpanded(height > 0.0f);
323        int newStackHeight = (int) height;
324        int itemHeight = getItemHeight();
325        int bottomStackPeekSize = mBottomStackPeekSize;
326        int minStackHeight = itemHeight + bottomStackPeekSize;
327        int stackHeight;
328        if (newStackHeight - mTopPadding >= minStackHeight) {
329            setTranslationY(0);
330            stackHeight = newStackHeight;
331        } else {
332
333            // We did not reach the position yet where we actually start growing,
334            // so we translate the stack upwards.
335            int translationY = (newStackHeight - minStackHeight);
336            // A slight parallax effect is introduced in order for the stack to catch up with
337            // the top card.
338            float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight;
339            partiallyThere = Math.max(0, partiallyThere);
340            translationY += (1 - partiallyThere) * bottomStackPeekSize;
341            setTranslationY(translationY - mTopPadding);
342            stackHeight = (int) (height - (translationY - mTopPadding));
343        }
344        if (stackHeight != mCurrentStackHeight) {
345            mCurrentStackHeight = stackHeight;
346            updateAlgorithmHeightAndPadding();
347            requestChildrenUpdate();
348        }
349    }
350
351    /**
352     * Get the current height of the view. This is at most the msize of the view given by a the
353     * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
354     *
355     * @return either the layout height or the externally defined height, whichever is smaller
356     */
357    private int getLayoutHeight() {
358        return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
359    }
360
361    public int getItemHeight() {
362        return mCollapsedSize;
363    }
364
365    public int getBottomStackPeekSize() {
366        return mBottomStackPeekSize;
367    }
368
369    public void setLongPressListener(View.OnLongClickListener listener) {
370        mSwipeHelper.setLongPressListener(listener);
371    }
372
373    public void onChildDismissed(View v) {
374        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
375        final View veto = v.findViewById(R.id.veto);
376        if (veto != null && veto.getVisibility() != View.GONE) {
377            veto.performClick();
378        }
379        setSwipingInProgress(false);
380        mSwipedOutViews.add(v);
381    }
382
383    public void onBeginDrag(View v) {
384        setSwipingInProgress(true);
385    }
386
387    public void onDragCancelled(View v) {
388        setSwipingInProgress(false);
389    }
390
391    public View getChildAtPosition(MotionEvent ev) {
392        return getChildAtPosition(ev.getX(), ev.getY());
393    }
394
395    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
396        int[] location = new int[2];
397        getLocationOnScreen(location);
398        return getChildAtPosition(touchX - location[0], touchY - location[1]);
399    }
400
401    public ExpandableView getChildAtPosition(float touchX, float touchY) {
402        // find the view under the pointer, accounting for GONE views
403        final int count = getChildCount();
404        for (int childIdx = 0; childIdx < count; childIdx++) {
405            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
406            if (slidingChild.getVisibility() == GONE) {
407                continue;
408            }
409            float top = slidingChild.getTranslationY();
410            float bottom = top + slidingChild.getActualHeight();
411            int left = slidingChild.getLeft();
412            int right = slidingChild.getRight();
413
414            if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
415                return slidingChild;
416            }
417        }
418        return null;
419    }
420
421    public boolean canChildBeExpanded(View v) {
422        return v instanceof ExpandableNotificationRow
423                && ((ExpandableNotificationRow) v).isExpandable();
424    }
425
426    public void setUserExpandedChild(View v, boolean userExpanded) {
427        if (v instanceof ExpandableNotificationRow) {
428            ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
429        }
430    }
431
432    public void setUserLockedChild(View v, boolean userLocked) {
433        if (v instanceof ExpandableNotificationRow) {
434            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
435        }
436    }
437
438    public View getChildContentView(View v) {
439        return v;
440    }
441
442    public boolean canChildBeDismissed(View v) {
443        final View veto = v.findViewById(R.id.veto);
444        return (veto != null && veto.getVisibility() != View.GONE);
445    }
446
447    private void setSwipingInProgress(boolean isSwiped) {
448        mSwipingInProgress = isSwiped;
449        if(isSwiped) {
450            requestDisallowInterceptTouchEvent(true);
451        }
452    }
453
454    @Override
455    protected void onConfigurationChanged(Configuration newConfig) {
456        super.onConfigurationChanged(newConfig);
457        float densityScale = getResources().getDisplayMetrics().density;
458        mSwipeHelper.setDensityScale(densityScale);
459        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
460        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
461        initView(getContext());
462    }
463
464    public void dismissRowAnimated(View child, int vel) {
465        mSwipeHelper.dismissChild(child, vel);
466    }
467
468    @Override
469    public boolean onTouchEvent(MotionEvent ev) {
470        boolean scrollerWantsIt = false;
471        if (!mSwipingInProgress) {
472            scrollerWantsIt = onScrollTouch(ev);
473        }
474        boolean horizontalSwipeWantsIt = false;
475        if (!mIsBeingDragged) {
476            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
477        }
478        return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev);
479    }
480
481    private boolean onScrollTouch(MotionEvent ev) {
482        initVelocityTrackerIfNotExists();
483        mVelocityTracker.addMovement(ev);
484
485        final int action = ev.getAction();
486
487        switch (action & MotionEvent.ACTION_MASK) {
488            case MotionEvent.ACTION_DOWN: {
489                if (getChildCount() == 0) {
490                    return false;
491                }
492                boolean isBeingDragged = !mScroller.isFinished();
493                setIsBeingDragged(isBeingDragged);
494
495                /*
496                 * If being flinged and user touches, stop the fling. isFinished
497                 * will be false if being flinged.
498                 */
499                if (!mScroller.isFinished()) {
500                    mScroller.abortAnimation();
501                }
502
503                // Remember where the motion event started
504                mLastMotionY = (int) ev.getY();
505                mActivePointerId = ev.getPointerId(0);
506                break;
507            }
508            case MotionEvent.ACTION_MOVE:
509                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
510                if (activePointerIndex == -1) {
511                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
512                    break;
513                }
514
515                final int y = (int) ev.getY(activePointerIndex);
516                int deltaY = mLastMotionY - y;
517                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
518                    setIsBeingDragged(true);
519                    if (deltaY > 0) {
520                        deltaY -= mTouchSlop;
521                    } else {
522                        deltaY += mTouchSlop;
523                    }
524                }
525                if (mIsBeingDragged) {
526                    // Scroll to follow the motion event
527                    mLastMotionY = y;
528
529                    final int oldX = mScrollX;
530                    final int oldY = mOwnScrollY;
531                    final int range = getScrollRange();
532                    final int overscrollMode = getOverScrollMode();
533                    final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
534                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
535
536                    // Calling overScrollBy will call onOverScrolled, which
537                    // calls onScrollChanged if applicable.
538                    if (overScrollBy(0, deltaY, 0, mOwnScrollY,
539                            0, range, 0, mOverscrollDistance, true)) {
540                        // Break our velocity if we hit a scroll barrier.
541                        mVelocityTracker.clear();
542                    }
543                    // TODO: Overscroll
544//                    if (canOverscroll) {
545//                        final int pulledToY = oldY + deltaY;
546//                        if (pulledToY < 0) {
547//                            mEdgeGlowTop.onPull((float) deltaY / getHeight());
548//                            if (!mEdgeGlowBottom.isFinished()) {
549//                                mEdgeGlowBottom.onRelease();
550//                            }
551//                        } else if (pulledToY > range) {
552//                            mEdgeGlowBottom.onPull((float) deltaY / getHeight());
553//                            if (!mEdgeGlowTop.isFinished()) {
554//                                mEdgeGlowTop.onRelease();
555//                            }
556//                        }
557//                        if (mEdgeGlowTop != null
558//                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){
559//                            postInvalidateOnAnimation();
560//                        }
561//                    }
562                }
563                break;
564            case MotionEvent.ACTION_UP:
565                if (mIsBeingDragged) {
566                    final VelocityTracker velocityTracker = mVelocityTracker;
567                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
568                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
569
570                    if (getChildCount() > 0) {
571                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
572                            fling(-initialVelocity);
573                        } else {
574                            if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
575                                    getScrollRange())) {
576                                postInvalidateOnAnimation();
577                            }
578                        }
579                    }
580
581                    mActivePointerId = INVALID_POINTER;
582                    endDrag();
583                }
584                break;
585            case MotionEvent.ACTION_CANCEL:
586                if (mIsBeingDragged && getChildCount() > 0) {
587                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
588                        postInvalidateOnAnimation();
589                    }
590                    mActivePointerId = INVALID_POINTER;
591                    endDrag();
592                }
593                break;
594            case MotionEvent.ACTION_POINTER_DOWN: {
595                final int index = ev.getActionIndex();
596                mLastMotionY = (int) ev.getY(index);
597                mActivePointerId = ev.getPointerId(index);
598                break;
599            }
600            case MotionEvent.ACTION_POINTER_UP:
601                onSecondaryPointerUp(ev);
602                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
603                break;
604        }
605        return true;
606    }
607
608    private void onSecondaryPointerUp(MotionEvent ev) {
609        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
610                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
611        final int pointerId = ev.getPointerId(pointerIndex);
612        if (pointerId == mActivePointerId) {
613            // This was our active pointer going up. Choose a new
614            // active pointer and adjust accordingly.
615            // TODO: Make this decision more intelligent.
616            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
617            mLastMotionY = (int) ev.getY(newPointerIndex);
618            mActivePointerId = ev.getPointerId(newPointerIndex);
619            if (mVelocityTracker != null) {
620                mVelocityTracker.clear();
621            }
622        }
623    }
624
625    private void initVelocityTrackerIfNotExists() {
626        if (mVelocityTracker == null) {
627            mVelocityTracker = VelocityTracker.obtain();
628        }
629    }
630
631    private void recycleVelocityTracker() {
632        if (mVelocityTracker != null) {
633            mVelocityTracker.recycle();
634            mVelocityTracker = null;
635        }
636    }
637
638    private void initOrResetVelocityTracker() {
639        if (mVelocityTracker == null) {
640            mVelocityTracker = VelocityTracker.obtain();
641        } else {
642            mVelocityTracker.clear();
643        }
644    }
645
646    @Override
647    public void computeScroll() {
648        if (mScroller.computeScrollOffset()) {
649            // This is called at drawing time by ViewGroup.
650            int oldX = mScrollX;
651            int oldY = mOwnScrollY;
652            int x = mScroller.getCurrX();
653            int y = mScroller.getCurrY();
654
655            if (oldX != x || oldY != y) {
656                final int range = getScrollRange();
657                final int overscrollMode = getOverScrollMode();
658                final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
659                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
660
661                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
662                        0, mOverflingDistance, false);
663                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
664
665                if (canOverscroll) {
666                    // TODO: Overscroll
667//                    if (y < 0 && oldY >= 0) {
668//                        mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
669//                    } else if (y > range && oldY <= range) {
670//                        mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
671//                    }
672                }
673                updateChildren();
674            }
675
676            // Keep on drawing until the animation has finished.
677            postInvalidateOnAnimation();
678        }
679    }
680
681    private void customScrollTo(int y) {
682        mOwnScrollY = y;
683        updateChildren();
684    }
685
686    @Override
687    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
688        // Treat animating scrolls differently; see #computeScroll() for why.
689        if (!mScroller.isFinished()) {
690            final int oldX = mScrollX;
691            final int oldY = mOwnScrollY;
692            mScrollX = scrollX;
693            mOwnScrollY = scrollY;
694            invalidateParentIfNeeded();
695            onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
696            if (clampedY) {
697                mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange());
698            }
699            updateChildren();
700        } else {
701            customScrollTo(scrollY);
702            scrollTo(scrollX, mScrollY);
703        }
704    }
705
706    private int getScrollRange() {
707        int scrollRange = 0;
708        ExpandableView firstChild = (ExpandableView) getFirstChildNotGone();
709        if (firstChild != null) {
710            int contentHeight = getContentHeight();
711            int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild);
712
713            scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize);
714            if (scrollRange > 0) {
715                View lastChild = getLastChildNotGone();
716                if (isViewExpanded(lastChild)) {
717                    // last child is expanded, so we have to ensure that it can exit the
718                    // bottom stack
719                    scrollRange += mCollapsedSize + mPaddingBetweenElements;
720                }
721                // We want to at least be able collapse the first item and not ending in a weird
722                // end state.
723                scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize);
724            }
725        }
726        return scrollRange;
727    }
728
729    /**
730     * @return the first child which has visibility unequal to GONE
731     */
732    private View getFirstChildNotGone() {
733        int childCount = getChildCount();
734        for (int i = 0; i < childCount; i++) {
735            View child = getChildAt(i);
736            if (child.getVisibility() != View.GONE) {
737                return child;
738            }
739        }
740        return null;
741    }
742
743    /**
744     * @return the last child which has visibility unequal to GONE
745     */
746    private View getLastChildNotGone() {
747        int childCount = getChildCount();
748        for (int i = childCount - 1; i >= 0; i--) {
749            View child = getChildAt(i);
750            if (child.getVisibility() != View.GONE) {
751                return child;
752            }
753        }
754        return null;
755    }
756
757    private int getMaxExpandHeight(View view) {
758        if (view instanceof ExpandableNotificationRow) {
759            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
760            return row.getMaximumAllowedExpandHeight();
761        }
762        return view.getHeight();
763    }
764
765    private int getContentHeight() {
766        return mContentHeight;
767    }
768
769    private void updateContentHeight() {
770        int height = 0;
771        for (int i = 0; i < getChildCount(); i++) {
772            View child = getChildAt(i);
773            if (child.getVisibility() != View.GONE) {
774                if (height != 0) {
775                    // add the padding before this element
776                    height += mPaddingBetweenElements;
777                }
778                if (child instanceof ExpandableNotificationRow) {
779                    ExpandableNotificationRow row = (ExpandableNotificationRow) child;
780                    height += row.getMaximumAllowedExpandHeight();
781                } else if (child instanceof ExpandableView) {
782                    ExpandableView expandableView = (ExpandableView) child;
783                    height += expandableView.getActualHeight();
784                }
785            }
786        }
787        mContentHeight = height + mTopPadding;
788    }
789
790    /**
791     * Fling the scroll view
792     *
793     * @param velocityY The initial velocity in the Y direction. Positive
794     *                  numbers mean that the finger/cursor is moving down the screen,
795     *                  which means we want to scroll towards the top.
796     */
797    private void fling(int velocityY) {
798        if (getChildCount() > 0) {
799            int height = (int) getLayoutHeight();
800            int bottom = getContentHeight();
801
802            mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0,
803                    Math.max(0, bottom - height), 0, height/2);
804
805            postInvalidateOnAnimation();
806        }
807    }
808
809    private void endDrag() {
810        setIsBeingDragged(false);
811
812        recycleVelocityTracker();
813
814        // TODO: Overscroll
815//        if (mEdgeGlowTop != null) {
816//            mEdgeGlowTop.onRelease();
817//            mEdgeGlowBottom.onRelease();
818//        }
819    }
820
821    @Override
822    public boolean onInterceptTouchEvent(MotionEvent ev) {
823        boolean scrollWantsIt = false;
824        if (!mSwipingInProgress) {
825            scrollWantsIt = onInterceptTouchEventScroll(ev);
826        }
827        boolean swipeWantsIt = false;
828        if (!mIsBeingDragged) {
829            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
830        }
831        return swipeWantsIt || scrollWantsIt ||
832                super.onInterceptTouchEvent(ev);
833    }
834
835    @Override
836    protected void onViewRemoved(View child) {
837        super.onViewRemoved(child);
838        ((ExpandableView) child).setOnHeightChangedListener(null);
839        mCurrentStackScrollState.removeViewStateForView(child);
840        mStackScrollAlgorithm.notifyChildrenChanged(this);
841        updateScrollStateForRemovedChild(child);
842        if (mIsExpanded) {
843
844            if (!mChildrenToAddAnimated.contains(child)) {
845                // Generate Animations
846                mChildrenToRemoveAnimated.add(child);
847                mNeedsAnimation = true;
848            } else {
849                mChildrenToAddAnimated.remove(child);
850            }
851        }
852    }
853
854    /**
855     * Updates the scroll position when a child was removed
856     *
857     * @param removedChild the removed child
858     */
859    private void updateScrollStateForRemovedChild(View removedChild) {
860        int startingPosition = getPositionInLinearLayout(removedChild);
861        int childHeight = removedChild.getHeight() + mPaddingBetweenElements;
862        int endPosition = startingPosition + childHeight;
863        if (endPosition <= mOwnScrollY) {
864            // This child is fully scrolled of the top, so we have to deduct its height from the
865            // scrollPosition
866            mOwnScrollY -= childHeight;
867        } else if (startingPosition < mOwnScrollY) {
868            // This child is currently being scrolled into, set the scroll position to the start of
869            // this child
870            mOwnScrollY = startingPosition;
871        }
872    }
873
874    private int getPositionInLinearLayout(View requestedChild) {
875        int position = 0;
876        for (int i = 0; i < getChildCount(); i++) {
877            View child = getChildAt(i);
878            if (child == requestedChild) {
879                return position;
880            }
881            if (child.getVisibility() != View.GONE) {
882                position += child.getHeight();
883                if (i < getChildCount()-1) {
884                    position += mPaddingBetweenElements;
885                }
886            }
887        }
888        return 0;
889    }
890
891    @Override
892    protected void onViewAdded(View child) {
893        super.onViewAdded(child);
894        mStackScrollAlgorithm.notifyChildrenChanged(this);
895        ((ExpandableView) child).setOnHeightChangedListener(this);
896        if (child.getVisibility() != View.GONE) {
897            generateAddAnimation(child);
898        }
899    }
900
901    public void generateAddAnimation(View child) {
902        if (mIsExpanded) {
903
904            // Generate Animations
905            mChildrenToAddAnimated.add(child);
906            mNeedsAnimation = true;
907        }
908    }
909
910    /**
911     * Change the position of child to a new location
912     *
913     * @param child the view to change the position for
914     * @param newIndex the new index
915     */
916    public void changeViewPosition(View child, int newIndex) {
917        if (child != null && child.getParent() == this) {
918            // TODO: handle this
919        }
920    }
921
922    private void startAnimationToState() {
923        if (mNeedsAnimation) {
924            generateChildHierarchyEvents();
925            mNeedsAnimation = false;
926        }
927        if (!mAnimationEvents.isEmpty()) {
928            mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState);
929        } else {
930            applyCurrentState();
931        }
932    }
933
934    private void generateChildHierarchyEvents() {
935        generateChildAdditionEvents();
936        generateChildRemovalEvents();
937        generateTopPaddingEvent();
938        mNeedsAnimation = false;
939    }
940
941    private void generateChildRemovalEvents() {
942        for (View  child : mChildrenToRemoveAnimated) {
943            boolean childWasSwipedOut = mSwipedOutViews.contains(child);
944            int animationType = childWasSwipedOut
945                    ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
946                    : AnimationEvent.ANIMATION_TYPE_REMOVE;
947            mAnimationEvents.add(new AnimationEvent(child, animationType));
948        }
949        mSwipedOutViews.clear();
950        mChildrenToRemoveAnimated.clear();
951    }
952
953    private void generateChildAdditionEvents() {
954        for (View  child : mChildrenToAddAnimated) {
955            mAnimationEvents.add(new AnimationEvent(child,
956                    AnimationEvent.ANIMATION_TYPE_ADD));
957        }
958        mChildrenToAddAnimated.clear();
959    }
960
961    private void generateTopPaddingEvent() {
962        if (mTopPaddingNeedsAnimation) {
963            mAnimationEvents.add(
964                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED));
965        }
966        mTopPaddingNeedsAnimation = false;
967    }
968
969    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
970        /*
971         * This method JUST determines whether we want to intercept the motion.
972         * If we return true, onMotionEvent will be called and we do the actual
973         * scrolling there.
974         */
975
976        /*
977        * Shortcut the most recurring case: the user is in the dragging
978        * state and he is moving his finger.  We want to intercept this
979        * motion.
980        */
981        final int action = ev.getAction();
982        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
983            return true;
984        }
985
986        /*
987         * Don't try to intercept touch if we can't scroll anyway.
988         */
989        if (mOwnScrollY == 0 && getScrollRange() == 0) {
990            return false;
991        }
992
993        switch (action & MotionEvent.ACTION_MASK) {
994            case MotionEvent.ACTION_MOVE: {
995                /*
996                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
997                 * whether the user has moved far enough from his original down touch.
998                 */
999
1000                /*
1001                * Locally do absolute value. mLastMotionY is set to the y value
1002                * of the down event.
1003                */
1004                final int activePointerId = mActivePointerId;
1005                if (activePointerId == INVALID_POINTER) {
1006                    // If we don't have a valid id, the touch down wasn't on content.
1007                    break;
1008                }
1009
1010                final int pointerIndex = ev.findPointerIndex(activePointerId);
1011                if (pointerIndex == -1) {
1012                    Log.e(TAG, "Invalid pointerId=" + activePointerId
1013                            + " in onInterceptTouchEvent");
1014                    break;
1015                }
1016
1017                final int y = (int) ev.getY(pointerIndex);
1018                final int yDiff = Math.abs(y - mLastMotionY);
1019                if (yDiff > mTouchSlop) {
1020                    setIsBeingDragged(true);
1021                    mLastMotionY = y;
1022                    initVelocityTrackerIfNotExists();
1023                    mVelocityTracker.addMovement(ev);
1024                }
1025                break;
1026            }
1027
1028            case MotionEvent.ACTION_DOWN: {
1029                final int y = (int) ev.getY();
1030                if (getChildAtPosition(ev.getX(), y) == null) {
1031                    setIsBeingDragged(false);
1032                    recycleVelocityTracker();
1033                    break;
1034                }
1035
1036                /*
1037                 * Remember location of down touch.
1038                 * ACTION_DOWN always refers to pointer index 0.
1039                 */
1040                mLastMotionY = y;
1041                mActivePointerId = ev.getPointerId(0);
1042
1043                initOrResetVelocityTracker();
1044                mVelocityTracker.addMovement(ev);
1045                /*
1046                * If being flinged and user touches the screen, initiate drag;
1047                * otherwise don't.  mScroller.isFinished should be false when
1048                * being flinged.
1049                */
1050                boolean isBeingDragged = !mScroller.isFinished();
1051                setIsBeingDragged(isBeingDragged);
1052                break;
1053            }
1054
1055            case MotionEvent.ACTION_CANCEL:
1056            case MotionEvent.ACTION_UP:
1057                /* Release the drag */
1058                setIsBeingDragged(false);
1059                mActivePointerId = INVALID_POINTER;
1060                recycleVelocityTracker();
1061                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
1062                    postInvalidateOnAnimation();
1063                }
1064                break;
1065            case MotionEvent.ACTION_POINTER_UP:
1066                onSecondaryPointerUp(ev);
1067                break;
1068        }
1069
1070        /*
1071        * The only time we want to intercept motion events is if we are in the
1072        * drag mode.
1073        */
1074        return mIsBeingDragged;
1075    }
1076
1077    private void setIsBeingDragged(boolean isDragged) {
1078        mIsBeingDragged = isDragged;
1079        if (isDragged) {
1080            requestDisallowInterceptTouchEvent(true);
1081            mSwipeHelper.removeLongPressCallback();
1082        }
1083    }
1084
1085    @Override
1086    public void onWindowFocusChanged(boolean hasWindowFocus) {
1087        super.onWindowFocusChanged(hasWindowFocus);
1088        if (!hasWindowFocus) {
1089            mSwipeHelper.removeLongPressCallback();
1090        }
1091    }
1092
1093    @Override
1094    public boolean isScrolledToTop() {
1095        return mOwnScrollY == 0;
1096    }
1097
1098    @Override
1099    public boolean isScrolledToBottom() {
1100        return mOwnScrollY >= getScrollRange();
1101    }
1102
1103    @Override
1104    public View getHostView() {
1105        return this;
1106    }
1107
1108    public int getEmptyBottomMargin() {
1109        int emptyMargin = mMaxLayoutHeight - mContentHeight;
1110        if (needsHeightAdaption()) {
1111            emptyMargin = emptyMargin - mCollapsedSize - mBottomStackPeekSize;
1112        }
1113        return Math.max(emptyMargin, 0);
1114    }
1115
1116    public void onExpansionStarted() {
1117        mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState);
1118    }
1119
1120    public void onExpansionStopped() {
1121        mStackScrollAlgorithm.onExpansionStopped();
1122    }
1123
1124    private void setIsExpanded(boolean isExpanded) {
1125        mIsExpanded = isExpanded;
1126        mStackScrollAlgorithm.setIsExpanded(isExpanded);
1127        if (!isExpanded) {
1128            mOwnScrollY = 0;
1129        }
1130    }
1131
1132    @Override
1133    public void onHeightChanged(ExpandableView view) {
1134        if (mListenForHeightChanges && !isCurrentlyAnimating()) {
1135            updateContentHeight();
1136            updateScrollPositionIfNecessary();
1137            if (mOnHeightChangedListener != null) {
1138                mOnHeightChangedListener.onHeightChanged(view);
1139            }
1140            requestChildrenUpdate();
1141        }
1142    }
1143
1144    public void setOnHeightChangedListener(
1145            ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
1146        this.mOnHeightChangedListener = mOnHeightChangedListener;
1147    }
1148
1149    public void onChildAnimationFinished() {
1150        requestChildrenUpdate();
1151        mAnimationEvents.clear();
1152    }
1153
1154    private void applyCurrentState() {
1155        mListenForHeightChanges = false;
1156        mCurrentStackScrollState.apply();
1157        mListenForHeightChanges = true;
1158        if (mListener != null) {
1159            mListener.onChildLocationsChanged(this);
1160        }
1161    }
1162
1163    /**
1164     * A listener that is notified when some child locations might have changed.
1165     */
1166    public interface OnChildLocationsChangedListener {
1167        public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
1168    }
1169
1170    static class AnimationEvent {
1171
1172        static int ANIMATION_TYPE_ADD = 1;
1173        static int ANIMATION_TYPE_REMOVE = 2;
1174        static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 3;
1175        static int ANIMATION_TYPE_TOP_PADDING_CHANGED = 4;
1176
1177        final long eventStartTime;
1178        final View changingView;
1179        final int animationType;
1180
1181        AnimationEvent(View view, int type) {
1182            eventStartTime = AnimationUtils.currentAnimationTimeMillis();
1183            changingView = view;
1184            animationType = type;
1185        }
1186    }
1187
1188}
1189