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