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