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