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