NotificationStackScrollLayout.java revision 8d9ff9c2c66bc1d3b92eb6992d58599ff80ed6dc
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    private static final float RUBBER_BAND_FACTOR = 0.35f;
57
58    /**
59     * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
60     */
61    private static final int INVALID_POINTER = -1;
62
63    private SwipeHelper mSwipeHelper;
64    private boolean mSwipingInProgress;
65    private int mCurrentStackHeight = Integer.MAX_VALUE;
66    private int mOwnScrollY;
67    private int mMaxLayoutHeight;
68
69    private VelocityTracker mVelocityTracker;
70    private OverScroller mScroller;
71    private int mTouchSlop;
72    private int mMinimumVelocity;
73    private int mMaximumVelocity;
74    private int mOverflingDistance;
75    private float mMaxOverScroll;
76    private boolean mIsBeingDragged;
77    private int mLastMotionY;
78    private int mActivePointerId;
79
80    private int mSidePaddings;
81    private Paint mDebugPaint;
82    private int mContentHeight;
83    private int mCollapsedSize;
84    private int mBottomStackSlowDownHeight;
85    private int mBottomStackPeekSize;
86    private int mEmptyMarginBottom;
87    private int mPaddingBetweenElements;
88    private int mPaddingBetweenElementsDimmed;
89    private int mPaddingBetweenElementsNormal;
90    private int mTopPadding;
91
92    /**
93     * The algorithm which calculates the properties for our children
94     */
95    private StackScrollAlgorithm mStackScrollAlgorithm;
96
97    /**
98     * The current State this Layout is in
99     */
100    private StackScrollState mCurrentStackScrollState = new StackScrollState(this);
101    private AmbientState mAmbientState = new AmbientState();
102    private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>();
103    private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>();
104    private ArrayList<View> mSnappedBackChildren = new ArrayList<View>();
105    private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>();
106    private ArrayList<AnimationEvent> mAnimationEvents
107            = new ArrayList<AnimationEvent>();
108    private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
109    private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
110
111    /**
112     * The raw amount of the overScroll on the top, which is not rubber-banded.
113     */
114    private float mOverScrolledTopPixels;
115
116    /**
117     * The raw amount of the overScroll on the bottom, which is not rubber-banded.
118     */
119    private float mOverScrolledBottomPixels;
120
121    private OnChildLocationsChangedListener mListener;
122    private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
123    private boolean mNeedsAnimation;
124    private boolean mTopPaddingNeedsAnimation;
125    private boolean mDimmedNeedsAnimation;
126    private boolean mActivateNeedsAnimation;
127    private boolean mIsExpanded = true;
128    private boolean mChildrenUpdateRequested;
129    private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
130            = new ViewTreeObserver.OnPreDrawListener() {
131        @Override
132        public boolean onPreDraw() {
133            updateChildren();
134            mChildrenUpdateRequested = false;
135            getViewTreeObserver().removeOnPreDrawListener(this);
136            return true;
137        }
138    };
139
140    public NotificationStackScrollLayout(Context context) {
141        this(context, null);
142    }
143
144    public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
145        this(context, attrs, 0);
146    }
147
148    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
149        this(context, attrs, defStyleAttr, 0);
150    }
151
152    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
153            int defStyleRes) {
154        super(context, attrs, defStyleAttr, defStyleRes);
155        initView(context);
156        if (DEBUG) {
157            setWillNotDraw(false);
158            mDebugPaint = new Paint();
159            mDebugPaint.setColor(0xffff0000);
160            mDebugPaint.setStrokeWidth(2);
161            mDebugPaint.setStyle(Paint.Style.STROKE);
162        }
163    }
164
165    @Override
166    protected void onDraw(Canvas canvas) {
167        if (DEBUG) {
168            int y = mCollapsedSize;
169            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
170            y = (int) (getLayoutHeight() - mBottomStackPeekSize
171                    - mBottomStackSlowDownHeight);
172            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
173            y = (int) (getLayoutHeight() - mBottomStackPeekSize);
174            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
175            y = (int) getLayoutHeight();
176            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
177        }
178    }
179
180    private void initView(Context context) {
181        mScroller = new OverScroller(getContext());
182        setFocusable(true);
183        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
184        setClipChildren(false);
185        final ViewConfiguration configuration = ViewConfiguration.get(context);
186        mTouchSlop = configuration.getScaledTouchSlop();
187        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
188        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
189        mOverflingDistance = configuration.getScaledOverflingDistance();
190        float densityScale = getResources().getDisplayMetrics().density;
191        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
192        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
193
194        mSidePaddings = context.getResources()
195                .getDimensionPixelSize(R.dimen.notification_side_padding);
196        mCollapsedSize = context.getResources()
197                .getDimensionPixelSize(R.dimen.notification_min_height);
198        mBottomStackPeekSize = context.getResources()
199                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
200        mEmptyMarginBottom = context.getResources().getDimensionPixelSize(
201                R.dimen.notification_stack_margin_bottom);
202        mStackScrollAlgorithm = new StackScrollAlgorithm(context);
203        mPaddingBetweenElementsDimmed = context.getResources()
204                .getDimensionPixelSize(R.dimen.notification_padding_dimmed);
205        mPaddingBetweenElementsNormal = context.getResources()
206                .getDimensionPixelSize(R.dimen.notification_padding);
207        updatePadding(false);
208    }
209
210    private void updatePadding(boolean dimmed) {
211        mPaddingBetweenElements = dimmed
212                ? mPaddingBetweenElementsDimmed
213                : mPaddingBetweenElementsNormal;
214        mBottomStackSlowDownHeight = mStackScrollAlgorithm.getBottomStackSlowDownLength();
215        updateContentHeight();
216    }
217
218    @Override
219    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
220        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
221        int mode = MeasureSpec.getMode(widthMeasureSpec);
222        int size = MeasureSpec.getSize(widthMeasureSpec);
223        int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode);
224        measureChildren(childMeasureSpec, heightMeasureSpec);
225    }
226
227    @Override
228    protected void onLayout(boolean changed, int l, int t, int r, int b) {
229
230        // we layout all our children centered on the top
231        float centerX = getWidth() / 2.0f;
232        for (int i = 0; i < getChildCount(); i++) {
233            View child = getChildAt(i);
234            float width = child.getMeasuredWidth();
235            float height = child.getMeasuredHeight();
236            child.layout((int) (centerX - width / 2.0f),
237                    0,
238                    (int) (centerX + width / 2.0f),
239                    (int) height);
240        }
241        setMaxLayoutHeight(getHeight() - mEmptyMarginBottom);
242        updateContentHeight();
243        updateScrollPositionIfNecessary();
244        requestChildrenUpdate();
245    }
246
247    public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
248        mListener = listener;
249    }
250
251    /**
252     * Returns the location the given child is currently rendered at.
253     *
254     * @param child the child to get the location for
255     * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants
256     */
257    public int getChildLocation(View child) {
258        ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child);
259        if (childViewState == null) {
260            return ViewState.LOCATION_UNKNOWN;
261        }
262        return childViewState.location;
263    }
264
265    private void setMaxLayoutHeight(int maxLayoutHeight) {
266        mMaxLayoutHeight = maxLayoutHeight;
267        updateAlgorithmHeightAndPadding();
268    }
269
270    private void updateAlgorithmHeightAndPadding() {
271        mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
272        mStackScrollAlgorithm.setTopPadding(mTopPadding);
273    }
274
275    /**
276     * @return whether the height of the layout needs to be adapted, in order to ensure that the
277     *         last child is not in the bottom stack.
278     */
279    private boolean needsHeightAdaption() {
280        View lastChild = getLastChildNotGone();
281        View firstChild = getFirstChildNotGone();
282        boolean isLastChildExpanded = isViewExpanded(lastChild);
283        return isLastChildExpanded && lastChild != firstChild;
284    }
285
286    private boolean isViewExpanded(View view) {
287        if (view != null) {
288            ExpandableView expandView = (ExpandableView) view;
289            return expandView.getActualHeight() > mCollapsedSize;
290        }
291        return false;
292    }
293
294    /**
295     * Updates the children views according to the stack scroll algorithm. Call this whenever
296     * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
297     */
298    private void updateChildren() {
299        mAmbientState.setScrollY(mOwnScrollY);
300        mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
301        if (!isCurrentlyAnimating() && !mNeedsAnimation) {
302            applyCurrentState();
303        } else {
304            startAnimationToState();
305        }
306    }
307
308    private void requestChildrenUpdate() {
309        if (!mChildrenUpdateRequested) {
310            getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
311            mChildrenUpdateRequested = true;
312            invalidate();
313        }
314    }
315
316    private boolean isCurrentlyAnimating() {
317        return mStateAnimator.isRunning();
318    }
319
320    private void updateScrollPositionIfNecessary() {
321        int scrollRange = getScrollRange();
322        if (scrollRange < mOwnScrollY) {
323            mOwnScrollY = scrollRange;
324        }
325    }
326
327    public int getTopPadding() {
328        return mTopPadding;
329    }
330
331    public void setTopPadding(int topPadding, boolean animate) {
332        if (mTopPadding != topPadding) {
333            mTopPadding = topPadding;
334            updateAlgorithmHeightAndPadding();
335            updateContentHeight();
336            if (animate) {
337                mTopPaddingNeedsAnimation = true;
338                mNeedsAnimation =  true;
339            }
340            requestChildrenUpdate();
341            if (mOnHeightChangedListener != null) {
342                mOnHeightChangedListener.onHeightChanged(null);
343            }
344        }
345    }
346
347    /**
348     * Update the height of the stack to a new height.
349     *
350     * @param height the new height of the stack
351     */
352    public void setStackHeight(float height) {
353        setIsExpanded(height > 0.0f);
354        int newStackHeight = (int) height;
355        int itemHeight = getItemHeight();
356        int bottomStackPeekSize = mBottomStackPeekSize;
357        int minStackHeight = itemHeight + bottomStackPeekSize;
358        int stackHeight;
359        if (newStackHeight - mTopPadding >= minStackHeight) {
360            setTranslationY(0);
361            stackHeight = newStackHeight;
362        } else {
363
364            // We did not reach the position yet where we actually start growing,
365            // so we translate the stack upwards.
366            int translationY = (newStackHeight - minStackHeight);
367            // A slight parallax effect is introduced in order for the stack to catch up with
368            // the top card.
369            float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight;
370            partiallyThere = Math.max(0, partiallyThere);
371            translationY += (1 - partiallyThere) * bottomStackPeekSize;
372            setTranslationY(translationY - mTopPadding);
373            stackHeight = (int) (height - (translationY - mTopPadding));
374        }
375        if (stackHeight != mCurrentStackHeight) {
376            mCurrentStackHeight = stackHeight;
377            updateAlgorithmHeightAndPadding();
378            requestChildrenUpdate();
379        }
380    }
381
382    /**
383     * Get the current height of the view. This is at most the msize of the view given by a the
384     * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
385     *
386     * @return either the layout height or the externally defined height, whichever is smaller
387     */
388    private int getLayoutHeight() {
389        return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
390    }
391
392    public int getItemHeight() {
393        return mCollapsedSize;
394    }
395
396    public int getBottomStackPeekSize() {
397        return mBottomStackPeekSize;
398    }
399
400    public void setLongPressListener(View.OnLongClickListener listener) {
401        mSwipeHelper.setLongPressListener(listener);
402    }
403
404    public void onChildDismissed(View v) {
405        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
406        final View veto = v.findViewById(R.id.veto);
407        if (veto != null && veto.getVisibility() != View.GONE) {
408            veto.performClick();
409        }
410        setSwipingInProgress(false);
411        if (mDragAnimPendingChildren.contains(v)) {
412            // We start the swipe and finish it in the same frame, we don't want any animation
413            // for the drag
414            mDragAnimPendingChildren.remove(v);
415        }
416        mSwipedOutViews.add(v);
417        mAmbientState.onDragFinished(v);
418    }
419
420    @Override
421    public void onChildSnappedBack(View animView) {
422        mAmbientState.onDragFinished(animView);
423        if (!mDragAnimPendingChildren.contains(animView)) {
424            mSnappedBackChildren.add(animView);
425            requestChildrenUpdate();
426            mNeedsAnimation = true;
427        } else {
428            // We start the swipe and snap back in the same frame, we don't want any animation
429            mDragAnimPendingChildren.remove(animView);
430        }
431    }
432
433    public void onBeginDrag(View v) {
434        setSwipingInProgress(true);
435        mDragAnimPendingChildren.add(v);
436        mAmbientState.onBeginDrag(v);
437        requestChildrenUpdate();
438        mNeedsAnimation = true;
439    }
440
441    public void onDragCancelled(View v) {
442        setSwipingInProgress(false);
443    }
444
445    public View getChildAtPosition(MotionEvent ev) {
446        return getChildAtPosition(ev.getX(), ev.getY());
447    }
448
449    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
450        int[] location = new int[2];
451        getLocationOnScreen(location);
452        return getChildAtPosition(touchX - location[0], touchY - location[1]);
453    }
454
455    public ExpandableView getChildAtPosition(float touchX, float touchY) {
456        // find the view under the pointer, accounting for GONE views
457        final int count = getChildCount();
458        for (int childIdx = 0; childIdx < count; childIdx++) {
459            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
460            if (slidingChild.getVisibility() == GONE) {
461                continue;
462            }
463            float top = slidingChild.getTranslationY();
464            float bottom = top + slidingChild.getActualHeight();
465            int left = slidingChild.getLeft();
466            int right = slidingChild.getRight();
467
468            if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
469                return slidingChild;
470            }
471        }
472        return null;
473    }
474
475    public boolean canChildBeExpanded(View v) {
476        return v instanceof ExpandableNotificationRow
477                && ((ExpandableNotificationRow) v).isExpandable();
478    }
479
480    public void setUserExpandedChild(View v, boolean userExpanded) {
481        if (v instanceof ExpandableNotificationRow) {
482            ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
483        }
484    }
485
486    public void setUserLockedChild(View v, boolean userLocked) {
487        if (v instanceof ExpandableNotificationRow) {
488            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
489        }
490    }
491
492    public View getChildContentView(View v) {
493        return v;
494    }
495
496    public boolean canChildBeDismissed(View v) {
497        final View veto = v.findViewById(R.id.veto);
498        return (veto != null && veto.getVisibility() != View.GONE);
499    }
500
501    private void setSwipingInProgress(boolean isSwiped) {
502        mSwipingInProgress = isSwiped;
503        if(isSwiped) {
504            requestDisallowInterceptTouchEvent(true);
505        }
506    }
507
508    @Override
509    protected void onConfigurationChanged(Configuration newConfig) {
510        super.onConfigurationChanged(newConfig);
511        float densityScale = getResources().getDisplayMetrics().density;
512        mSwipeHelper.setDensityScale(densityScale);
513        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
514        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
515        initView(getContext());
516    }
517
518    public void dismissRowAnimated(View child, int vel) {
519        mSwipeHelper.dismissChild(child, vel);
520    }
521
522    @Override
523    public boolean onTouchEvent(MotionEvent ev) {
524        if (!isEnabled()) {
525            return false;
526        }
527        boolean scrollerWantsIt = false;
528        if (!mSwipingInProgress) {
529            scrollerWantsIt = onScrollTouch(ev);
530        }
531        boolean horizontalSwipeWantsIt = false;
532        if (!mIsBeingDragged) {
533            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
534        }
535        return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev);
536    }
537
538    private boolean onScrollTouch(MotionEvent ev) {
539        initVelocityTrackerIfNotExists();
540        mVelocityTracker.addMovement(ev);
541
542        final int action = ev.getAction();
543
544        switch (action & MotionEvent.ACTION_MASK) {
545            case MotionEvent.ACTION_DOWN: {
546                if (getChildCount() == 0 || !isInContentBounds(ev)) {
547                    return false;
548                }
549                boolean isBeingDragged = !mScroller.isFinished();
550                setIsBeingDragged(isBeingDragged);
551
552                /*
553                 * If being flinged and user touches, stop the fling. isFinished
554                 * will be false if being flinged.
555                 */
556                if (!mScroller.isFinished()) {
557                    mScroller.forceFinished(true);
558                }
559
560                // Remember where the motion event started
561                mLastMotionY = (int) ev.getY();
562                mActivePointerId = ev.getPointerId(0);
563                break;
564            }
565            case MotionEvent.ACTION_MOVE:
566                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
567                if (activePointerIndex == -1) {
568                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
569                    break;
570                }
571
572                final int y = (int) ev.getY(activePointerIndex);
573                int deltaY = mLastMotionY - y;
574                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
575                    setIsBeingDragged(true);
576                    if (deltaY > 0) {
577                        deltaY -= mTouchSlop;
578                    } else {
579                        deltaY += mTouchSlop;
580                    }
581                }
582                if (mIsBeingDragged) {
583                    // Scroll to follow the motion event
584                    mLastMotionY = y;
585                    final int range = getScrollRange();
586
587                    float scrollAmount;
588                    if (deltaY < 0) {
589                        scrollAmount = overScrollDown(deltaY);
590                    } else {
591                        scrollAmount = overScrollUp(deltaY, range);
592                    }
593
594                    // Calling overScrollBy will call onOverScrolled, which
595                    // calls onScrollChanged if applicable.
596                    if (scrollAmount != 0.0f) {
597                        // The scrolling motion could not be compensated with the
598                        // existing overScroll, we have to scroll the view
599                        overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY,
600                                0, range, 0, getHeight() / 2, true);
601                    }
602                }
603                break;
604            case MotionEvent.ACTION_UP:
605                if (mIsBeingDragged) {
606                    final VelocityTracker velocityTracker = mVelocityTracker;
607                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
608                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
609
610                    if (getChildCount() > 0) {
611                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
612                            fling(-initialVelocity);
613                        } else {
614                            if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
615                                    getScrollRange())) {
616                                postInvalidateOnAnimation();
617                            }
618                        }
619                    }
620
621                    mActivePointerId = INVALID_POINTER;
622                    endDrag();
623                }
624                break;
625            case MotionEvent.ACTION_CANCEL:
626                if (mIsBeingDragged && getChildCount() > 0) {
627                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
628                        postInvalidateOnAnimation();
629                    }
630                    mActivePointerId = INVALID_POINTER;
631                    endDrag();
632                }
633                break;
634            case MotionEvent.ACTION_POINTER_DOWN: {
635                final int index = ev.getActionIndex();
636                mLastMotionY = (int) ev.getY(index);
637                mActivePointerId = ev.getPointerId(index);
638                break;
639            }
640            case MotionEvent.ACTION_POINTER_UP:
641                onSecondaryPointerUp(ev);
642                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
643                break;
644        }
645        return true;
646    }
647
648    /**
649     * Perform a scroll upwards and adapt the overscroll amounts accordingly
650     *
651     * @param deltaY The amount to scroll upwards, has to be positive.
652     * @return The amount of scrolling to be performed by the scroller,
653     *         not handled by the overScroll amount.
654     */
655    private float overScrollUp(int deltaY, int range) {
656        deltaY = Math.max(deltaY, 0);
657        float currentTopAmount = getCurrentOverScrollAmount(true);
658        float newTopAmount = currentTopAmount - deltaY;
659        if (currentTopAmount > 0) {
660            setOverScrollAmount(newTopAmount, true /* onTop */,
661                    false /* animate */);
662        }
663        // Top overScroll might not grab all scrolling motion,
664        // we have to scroll as well.
665        float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
666        float newScrollY = mOwnScrollY + scrollAmount;
667        if (newScrollY > range) {
668            float currentBottomPixels = getCurrentOverScrolledPixels(false);
669            // We overScroll on the top
670            setOverScrolledPixels(currentBottomPixels + newScrollY - range,
671                    false /* onTop */,
672                    false /* animate */);
673            mOwnScrollY = range;
674            scrollAmount = 0.0f;
675        }
676        return scrollAmount;
677    }
678
679    /**
680     * Perform a scroll downward and adapt the overscroll amounts accordingly
681     *
682     * @param deltaY The amount to scroll downwards, has to be negative.
683     * @return The amount of scrolling to be performed by the scroller,
684     *         not handled by the overScroll amount.
685     */
686    private float overScrollDown(int deltaY) {
687        deltaY = Math.min(deltaY, 0);
688        float currentBottomAmount = getCurrentOverScrollAmount(false);
689        float newBottomAmount = currentBottomAmount + deltaY;
690        if (currentBottomAmount > 0) {
691            setOverScrollAmount(newBottomAmount, false /* onTop */,
692                    false /* animate */);
693        }
694        // Bottom overScroll might not grab all scrolling motion,
695        // we have to scroll as well.
696        float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
697        float newScrollY = mOwnScrollY + scrollAmount;
698        if (newScrollY < 0) {
699            float currentTopPixels = getCurrentOverScrolledPixels(true);
700            // We overScroll on the top
701            setOverScrolledPixels(currentTopPixels - newScrollY,
702                    true /* onTop */,
703                    false /* animate */);
704            mOwnScrollY = 0;
705            scrollAmount = 0.0f;
706        }
707        return scrollAmount;
708    }
709
710    private void onSecondaryPointerUp(MotionEvent ev) {
711        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
712                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
713        final int pointerId = ev.getPointerId(pointerIndex);
714        if (pointerId == mActivePointerId) {
715            // This was our active pointer going up. Choose a new
716            // active pointer and adjust accordingly.
717            // TODO: Make this decision more intelligent.
718            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
719            mLastMotionY = (int) ev.getY(newPointerIndex);
720            mActivePointerId = ev.getPointerId(newPointerIndex);
721            if (mVelocityTracker != null) {
722                mVelocityTracker.clear();
723            }
724        }
725    }
726
727    private void initVelocityTrackerIfNotExists() {
728        if (mVelocityTracker == null) {
729            mVelocityTracker = VelocityTracker.obtain();
730        }
731    }
732
733    private void recycleVelocityTracker() {
734        if (mVelocityTracker != null) {
735            mVelocityTracker.recycle();
736            mVelocityTracker = null;
737        }
738    }
739
740    private void initOrResetVelocityTracker() {
741        if (mVelocityTracker == null) {
742            mVelocityTracker = VelocityTracker.obtain();
743        } else {
744            mVelocityTracker.clear();
745        }
746    }
747
748    @Override
749    public void computeScroll() {
750        if (mScroller.computeScrollOffset()) {
751            // This is called at drawing time by ViewGroup.
752            int oldX = mScrollX;
753            int oldY = mOwnScrollY;
754            int x = mScroller.getCurrX();
755            int y = mScroller.getCurrY();
756
757            if (oldX != x || oldY != y) {
758                final int range = getScrollRange();
759                if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
760                    float currVelocity = mScroller.getCurrVelocity();
761                    if (currVelocity >= mMinimumVelocity * 20) {
762                        mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
763                    }
764                }
765
766                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
767                        0, (int) (mMaxOverScroll), false);
768                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
769            }
770
771            // Keep on drawing until the animation has finished.
772            postInvalidateOnAnimation();
773        }
774    }
775
776    @Override
777    protected int computeVerticalScrollRange() {
778        // needed for the overScroller
779        return mContentHeight;
780    }
781
782    /**
783     * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded
784     * overscroll effect based on numPixels. By default this will also cancel animations on the
785     * same overScroll edge.
786     *
787     * @param numPixels The amount of pixels to overScroll by. These will be scaled according to
788     *                  the rubber-banding logic.
789     * @param onTop Should the effect be applied on top of the scroller.
790     * @param animate Should an animation be performed.
791     */
792    public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) {
793        setOverScrollAmount(numPixels * RUBBER_BAND_FACTOR, onTop, animate, true);
794    }
795
796    /**
797     * Set the effective overScroll amount which will be directly reflected in the layout.
798     * By default this will also cancel animations on the same overScroll edge.
799     *
800     * @param amount The amount to overScroll by.
801     * @param onTop Should the effect be applied on top of the scroller.
802     * @param animate Should an animation be performed.
803     */
804    public void setOverScrollAmount(float amount, boolean onTop, boolean animate) {
805        setOverScrollAmount(amount, onTop, animate, true);
806    }
807
808    /**
809     * Set the effective overScroll amount which will be directly reflected in the layout.
810     *
811     * @param amount The amount to overScroll by.
812     * @param onTop Should the effect be applied on top of the scroller.
813     * @param animate Should an animation be performed.
814     * @param cancelAnimators Should running animations be cancelled.
815     */
816    public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
817            boolean cancelAnimators) {
818        if (cancelAnimators) {
819            mStateAnimator.cancelOverScrollAnimators(onTop);
820        }
821        setOverScrollAmountInternal(amount, onTop, animate);
822    }
823
824    private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate) {
825        amount = Math.max(0, amount);
826        if (animate) {
827            mStateAnimator.animateOverScrollToAmount(amount, onTop);
828        } else {
829            setOverScrolledPixels(amount / RUBBER_BAND_FACTOR, onTop);
830            mAmbientState.setOverScrollAmount(amount, onTop);
831            requestChildrenUpdate();
832        }
833    }
834
835    public float getCurrentOverScrollAmount(boolean top) {
836        return mAmbientState.getOverScrollAmount(top);
837    }
838
839    public float getCurrentOverScrolledPixels(boolean top) {
840        return top? mOverScrolledTopPixels : mOverScrolledBottomPixels;
841    }
842
843    private void setOverScrolledPixels(float amount, boolean onTop) {
844        if (onTop) {
845            mOverScrolledTopPixels = amount;
846        } else {
847            mOverScrolledBottomPixels = amount;
848        }
849    }
850
851    private void customScrollTo(int y) {
852        mOwnScrollY = y;
853        updateChildren();
854    }
855
856    @Override
857    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
858        // Treat animating scrolls differently; see #computeScroll() for why.
859        if (!mScroller.isFinished()) {
860            final int oldX = mScrollX;
861            final int oldY = mOwnScrollY;
862            mScrollX = scrollX;
863            mOwnScrollY = scrollY;
864            if (clampedY) {
865                springBack();
866            } else {
867                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
868                invalidateParentIfNeeded();
869                updateChildren();
870            }
871        } else {
872            customScrollTo(scrollY);
873            scrollTo(scrollX, mScrollY);
874        }
875    }
876
877    private void springBack() {
878        int scrollRange = getScrollRange();
879        boolean overScrolledTop = mOwnScrollY <= 0;
880        boolean overScrolledBottom = mOwnScrollY >= scrollRange;
881        if (overScrolledTop || overScrolledBottom) {
882            boolean onTop;
883            float newAmount;
884            if (overScrolledTop) {
885                onTop = true;
886                newAmount = -mOwnScrollY;
887                mOwnScrollY = 0;
888            } else {
889                onTop = false;
890                newAmount = mOwnScrollY - scrollRange;
891                mOwnScrollY = scrollRange;
892            }
893            setOverScrollAmount(newAmount, onTop, false);
894            setOverScrollAmount(0.0f, onTop, true);
895            mScroller.forceFinished(true);
896        }
897    }
898
899    private int getScrollRange() {
900        int scrollRange = 0;
901        ExpandableView firstChild = (ExpandableView) getFirstChildNotGone();
902        if (firstChild != null) {
903            int contentHeight = getContentHeight();
904            int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild);
905            scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize
906                    + mBottomStackSlowDownHeight);
907            if (scrollRange > 0) {
908                View lastChild = getLastChildNotGone();
909                // We want to at least be able collapse the first item and not ending in a weird
910                // end state.
911                scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize);
912            }
913        }
914        return scrollRange;
915    }
916
917    /**
918     * @return the first child which has visibility unequal to GONE
919     */
920    private View getFirstChildNotGone() {
921        int childCount = getChildCount();
922        for (int i = 0; i < childCount; i++) {
923            View child = getChildAt(i);
924            if (child.getVisibility() != View.GONE) {
925                return child;
926            }
927        }
928        return null;
929    }
930
931    /**
932     * @return the last child which has visibility unequal to GONE
933     */
934    private View getLastChildNotGone() {
935        int childCount = getChildCount();
936        for (int i = childCount - 1; i >= 0; i--) {
937            View child = getChildAt(i);
938            if (child.getVisibility() != View.GONE) {
939                return child;
940            }
941        }
942        return null;
943    }
944
945    private int getMaxExpandHeight(View view) {
946        if (view instanceof ExpandableNotificationRow) {
947            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
948            return row.getIntrinsicHeight();
949        }
950        return view.getHeight();
951    }
952
953    private int getContentHeight() {
954        return mContentHeight;
955    }
956
957    private void updateContentHeight() {
958        int height = 0;
959        for (int i = 0; i < getChildCount(); i++) {
960            View child = getChildAt(i);
961            if (child.getVisibility() != View.GONE) {
962                if (height != 0) {
963                    // add the padding before this element
964                    height += mPaddingBetweenElements;
965                }
966                if (child instanceof ExpandableNotificationRow) {
967                    ExpandableNotificationRow row = (ExpandableNotificationRow) child;
968                    height += row.getIntrinsicHeight();
969                } else if (child instanceof ExpandableView) {
970                    ExpandableView expandableView = (ExpandableView) child;
971                    height += expandableView.getActualHeight();
972                }
973            }
974        }
975        mContentHeight = height + mTopPadding;
976    }
977
978    /**
979     * Fling the scroll view
980     *
981     * @param velocityY The initial velocity in the Y direction. Positive
982     *                  numbers mean that the finger/cursor is moving down the screen,
983     *                  which means we want to scroll towards the top.
984     */
985    private void fling(int velocityY) {
986        if (getChildCount() > 0) {
987            int height = (int) getLayoutHeight();
988            int bottom = getContentHeight();
989
990            float topAmount = getCurrentOverScrollAmount(true);
991            float bottomAmount = getCurrentOverScrollAmount(false);
992            if (velocityY < 0 && topAmount > 0) {
993                mOwnScrollY -= (int) topAmount;
994                setOverScrollAmount(0, true, false);
995                mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR
996                        * mOverflingDistance + topAmount;
997            } else if (velocityY > 0 && bottomAmount > 0) {
998                mOwnScrollY += bottomAmount;
999                setOverScrollAmount(0, false, false);
1000                mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR
1001                        * mOverflingDistance + bottomAmount;
1002            } else {
1003                // it will be set once we reach the boundary
1004                mMaxOverScroll = 0.0f;
1005            }
1006            mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0,
1007                    Math.max(0, bottom - height), 0, height/2);
1008
1009            postInvalidateOnAnimation();
1010        }
1011    }
1012
1013    private void endDrag() {
1014        setIsBeingDragged(false);
1015
1016        recycleVelocityTracker();
1017
1018        if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
1019            setOverScrollAmount(0, true /* onTop */, true /* animate */);
1020        }
1021        if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
1022            setOverScrollAmount(0, false /* onTop */, true /* animate */);
1023        }
1024    }
1025
1026    @Override
1027    public boolean onInterceptTouchEvent(MotionEvent ev) {
1028        boolean scrollWantsIt = false;
1029        if (!mSwipingInProgress) {
1030            scrollWantsIt = onInterceptTouchEventScroll(ev);
1031        }
1032        boolean swipeWantsIt = false;
1033        if (!mIsBeingDragged) {
1034            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
1035        }
1036        return swipeWantsIt || scrollWantsIt ||
1037                super.onInterceptTouchEvent(ev);
1038    }
1039
1040    @Override
1041    protected void onViewRemoved(View child) {
1042        super.onViewRemoved(child);
1043        ((ExpandableView) child).setOnHeightChangedListener(null);
1044        mCurrentStackScrollState.removeViewStateForView(child);
1045        mStackScrollAlgorithm.notifyChildrenChanged(this);
1046        updateScrollStateForRemovedChild(child);
1047        if (mIsExpanded) {
1048
1049            if (!mChildrenToAddAnimated.contains(child)) {
1050                // Generate Animations
1051                mChildrenToRemoveAnimated.add(child);
1052                mNeedsAnimation = true;
1053            } else {
1054                mChildrenToAddAnimated.remove(child);
1055            }
1056        }
1057    }
1058
1059    /**
1060     * Updates the scroll position when a child was removed
1061     *
1062     * @param removedChild the removed child
1063     */
1064    private void updateScrollStateForRemovedChild(View removedChild) {
1065        int startingPosition = getPositionInLinearLayout(removedChild);
1066        int childHeight = removedChild.getHeight() + mPaddingBetweenElements;
1067        int endPosition = startingPosition + childHeight;
1068        if (endPosition <= mOwnScrollY) {
1069            // This child is fully scrolled of the top, so we have to deduct its height from the
1070            // scrollPosition
1071            mOwnScrollY -= childHeight;
1072        } else if (startingPosition < mOwnScrollY) {
1073            // This child is currently being scrolled into, set the scroll position to the start of
1074            // this child
1075            mOwnScrollY = startingPosition;
1076        }
1077    }
1078
1079    private int getPositionInLinearLayout(View requestedChild) {
1080        int position = 0;
1081        for (int i = 0; i < getChildCount(); i++) {
1082            View child = getChildAt(i);
1083            if (child == requestedChild) {
1084                return position;
1085            }
1086            if (child.getVisibility() != View.GONE) {
1087                position += child.getHeight();
1088                if (i < getChildCount()-1) {
1089                    position += mPaddingBetweenElements;
1090                }
1091            }
1092        }
1093        return 0;
1094    }
1095
1096    @Override
1097    protected void onViewAdded(View child) {
1098        super.onViewAdded(child);
1099        mStackScrollAlgorithm.notifyChildrenChanged(this);
1100        ((ExpandableView) child).setOnHeightChangedListener(this);
1101        if (child.getVisibility() != View.GONE) {
1102            generateAddAnimation(child);
1103        }
1104    }
1105
1106    public void generateAddAnimation(View child) {
1107        if (mIsExpanded) {
1108
1109            // Generate Animations
1110            mChildrenToAddAnimated.add(child);
1111            mNeedsAnimation = true;
1112        }
1113    }
1114
1115    /**
1116     * Change the position of child to a new location
1117     *
1118     * @param child the view to change the position for
1119     * @param newIndex the new index
1120     */
1121    public void changeViewPosition(View child, int newIndex) {
1122        if (child != null && child.getParent() == this) {
1123            // TODO: handle this
1124        }
1125    }
1126
1127    private void startAnimationToState() {
1128        if (mNeedsAnimation) {
1129            generateChildHierarchyEvents();
1130            mNeedsAnimation = false;
1131        }
1132        if (!mAnimationEvents.isEmpty()) {
1133            mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState);
1134        } else {
1135            applyCurrentState();
1136        }
1137    }
1138
1139    private void generateChildHierarchyEvents() {
1140        generateChildAdditionEvents();
1141        generateChildRemovalEvents();
1142        generateSnapBackEvents();
1143        generateDragEvents();
1144        generateTopPaddingEvent();
1145        generateActivateEvent();
1146        generateDimmedEvent();
1147        mNeedsAnimation = false;
1148    }
1149
1150    private void generateSnapBackEvents() {
1151        for (View child : mSnappedBackChildren) {
1152            mAnimationEvents.add(new AnimationEvent(child,
1153                    AnimationEvent.ANIMATION_TYPE_SNAP_BACK));
1154        }
1155        mSnappedBackChildren.clear();
1156    }
1157
1158    private void generateDragEvents() {
1159        for (View child : mDragAnimPendingChildren) {
1160            mAnimationEvents.add(new AnimationEvent(child,
1161                    AnimationEvent.ANIMATION_TYPE_START_DRAG));
1162        }
1163        mDragAnimPendingChildren.clear();
1164    }
1165
1166    private void generateChildRemovalEvents() {
1167        for (View child : mChildrenToRemoveAnimated) {
1168            boolean childWasSwipedOut = mSwipedOutViews.contains(child);
1169            int animationType = childWasSwipedOut
1170                    ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
1171                    : AnimationEvent.ANIMATION_TYPE_REMOVE;
1172            mAnimationEvents.add(new AnimationEvent(child, animationType));
1173        }
1174        mSwipedOutViews.clear();
1175        mChildrenToRemoveAnimated.clear();
1176    }
1177
1178    private void generateChildAdditionEvents() {
1179        for (View child : mChildrenToAddAnimated) {
1180            mAnimationEvents.add(new AnimationEvent(child,
1181                    AnimationEvent.ANIMATION_TYPE_ADD));
1182        }
1183        mChildrenToAddAnimated.clear();
1184    }
1185
1186    private void generateTopPaddingEvent() {
1187        if (mTopPaddingNeedsAnimation) {
1188            mAnimationEvents.add(
1189                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED));
1190        }
1191        mTopPaddingNeedsAnimation = false;
1192    }
1193
1194    private void generateActivateEvent() {
1195        if (mActivateNeedsAnimation) {
1196            mAnimationEvents.add(
1197                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD));
1198        }
1199        mActivateNeedsAnimation = false;
1200    }
1201
1202    private void generateDimmedEvent() {
1203        if (mDimmedNeedsAnimation) {
1204            mAnimationEvents.add(
1205                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED));
1206        }
1207        mDimmedNeedsAnimation = false;
1208    }
1209
1210    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
1211        /*
1212         * This method JUST determines whether we want to intercept the motion.
1213         * If we return true, onMotionEvent will be called and we do the actual
1214         * scrolling there.
1215         */
1216
1217        /*
1218        * Shortcut the most recurring case: the user is in the dragging
1219        * state and he is moving his finger.  We want to intercept this
1220        * motion.
1221        */
1222        final int action = ev.getAction();
1223        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
1224            return true;
1225        }
1226
1227        /*
1228         * Don't try to intercept touch if we can't scroll anyway.
1229         */
1230        if (mOwnScrollY == 0 && getScrollRange() == 0) {
1231            return false;
1232        }
1233
1234        switch (action & MotionEvent.ACTION_MASK) {
1235            case MotionEvent.ACTION_MOVE: {
1236                /*
1237                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
1238                 * whether the user has moved far enough from his original down touch.
1239                 */
1240
1241                /*
1242                * Locally do absolute value. mLastMotionY is set to the y value
1243                * of the down event.
1244                */
1245                final int activePointerId = mActivePointerId;
1246                if (activePointerId == INVALID_POINTER) {
1247                    // If we don't have a valid id, the touch down wasn't on content.
1248                    break;
1249                }
1250
1251                final int pointerIndex = ev.findPointerIndex(activePointerId);
1252                if (pointerIndex == -1) {
1253                    Log.e(TAG, "Invalid pointerId=" + activePointerId
1254                            + " in onInterceptTouchEvent");
1255                    break;
1256                }
1257
1258                final int y = (int) ev.getY(pointerIndex);
1259                final int yDiff = Math.abs(y - mLastMotionY);
1260                if (yDiff > mTouchSlop) {
1261                    setIsBeingDragged(true);
1262                    mLastMotionY = y;
1263                    initVelocityTrackerIfNotExists();
1264                    mVelocityTracker.addMovement(ev);
1265                }
1266                break;
1267            }
1268
1269            case MotionEvent.ACTION_DOWN: {
1270                final int y = (int) ev.getY();
1271                if (getChildAtPosition(ev.getX(), y) == null) {
1272                    setIsBeingDragged(false);
1273                    recycleVelocityTracker();
1274                    break;
1275                }
1276
1277                /*
1278                 * Remember location of down touch.
1279                 * ACTION_DOWN always refers to pointer index 0.
1280                 */
1281                mLastMotionY = y;
1282                mActivePointerId = ev.getPointerId(0);
1283
1284                initOrResetVelocityTracker();
1285                mVelocityTracker.addMovement(ev);
1286                /*
1287                * If being flinged and user touches the screen, initiate drag;
1288                * otherwise don't.  mScroller.isFinished should be false when
1289                * being flinged.
1290                */
1291                boolean isBeingDragged = !mScroller.isFinished();
1292                setIsBeingDragged(isBeingDragged);
1293                break;
1294            }
1295
1296            case MotionEvent.ACTION_CANCEL:
1297            case MotionEvent.ACTION_UP:
1298                /* Release the drag */
1299                setIsBeingDragged(false);
1300                mActivePointerId = INVALID_POINTER;
1301                recycleVelocityTracker();
1302                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
1303                    postInvalidateOnAnimation();
1304                }
1305                break;
1306            case MotionEvent.ACTION_POINTER_UP:
1307                onSecondaryPointerUp(ev);
1308                break;
1309        }
1310
1311        /*
1312        * The only time we want to intercept motion events is if we are in the
1313        * drag mode.
1314        */
1315        return mIsBeingDragged;
1316    }
1317
1318    /**
1319     * @return Whether the specified motion event is actually happening over the content.
1320     */
1321    private boolean isInContentBounds(MotionEvent event) {
1322        return event.getY() < getHeight() - getEmptyBottomMargin();
1323    }
1324
1325    private void setIsBeingDragged(boolean isDragged) {
1326        mIsBeingDragged = isDragged;
1327        if (isDragged) {
1328            requestDisallowInterceptTouchEvent(true);
1329            mSwipeHelper.removeLongPressCallback();
1330        }
1331    }
1332
1333    @Override
1334    public void onWindowFocusChanged(boolean hasWindowFocus) {
1335        super.onWindowFocusChanged(hasWindowFocus);
1336        if (!hasWindowFocus) {
1337            mSwipeHelper.removeLongPressCallback();
1338        }
1339    }
1340
1341    @Override
1342    public boolean isScrolledToTop() {
1343        return mOwnScrollY == 0;
1344    }
1345
1346    @Override
1347    public boolean isScrolledToBottom() {
1348        return mOwnScrollY >= getScrollRange();
1349    }
1350
1351    @Override
1352    public View getHostView() {
1353        return this;
1354    }
1355
1356    public int getEmptyBottomMargin() {
1357        int emptyMargin = mMaxLayoutHeight - mContentHeight;
1358        if (needsHeightAdaption()) {
1359            emptyMargin = emptyMargin - mBottomStackSlowDownHeight - mBottomStackPeekSize;
1360        }
1361        return Math.max(emptyMargin, 0);
1362    }
1363
1364    public void onExpansionStarted() {
1365        mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState);
1366    }
1367
1368    public void onExpansionStopped() {
1369        mStackScrollAlgorithm.onExpansionStopped();
1370    }
1371
1372    private void setIsExpanded(boolean isExpanded) {
1373        mIsExpanded = isExpanded;
1374        mStackScrollAlgorithm.setIsExpanded(isExpanded);
1375        if (!isExpanded) {
1376            mOwnScrollY = 0;
1377        }
1378    }
1379
1380    @Override
1381    public void onHeightChanged(ExpandableView view) {
1382        updateContentHeight();
1383        updateScrollPositionIfNecessary();
1384        if (mOnHeightChangedListener != null) {
1385            mOnHeightChangedListener.onHeightChanged(view);
1386        }
1387        requestChildrenUpdate();
1388    }
1389
1390    public void setOnHeightChangedListener(
1391            ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
1392        this.mOnHeightChangedListener = mOnHeightChangedListener;
1393    }
1394
1395    public void onChildAnimationFinished() {
1396        requestChildrenUpdate();
1397        mAnimationEvents.clear();
1398    }
1399
1400    /**
1401     * See {@link AmbientState#setDimmed}.
1402     */
1403    public void setDimmed(boolean dimmed, boolean animate) {
1404        mStackScrollAlgorithm.setDimmed(dimmed);
1405        mAmbientState.setDimmed(dimmed);
1406        updatePadding(dimmed);
1407        if (animate) {
1408            mDimmedNeedsAnimation = true;
1409            mNeedsAnimation =  true;
1410        }
1411        requestChildrenUpdate();
1412    }
1413
1414    /**
1415     * See {@link AmbientState#setActivatedChild}.
1416     */
1417    public void setActivatedChild(View activatedChild) {
1418        mAmbientState.setActivatedChild(activatedChild);
1419        mActivateNeedsAnimation = true;
1420        mNeedsAnimation =  true;
1421        requestChildrenUpdate();
1422    }
1423
1424    public View getActivatedChild() {
1425        return mAmbientState.getActivatedChild();
1426    }
1427
1428    private void applyCurrentState() {
1429        mCurrentStackScrollState.apply();
1430        if (mListener != null) {
1431            mListener.onChildLocationsChanged(this);
1432        }
1433    }
1434
1435    /**
1436     * A listener that is notified when some child locations might have changed.
1437     */
1438    public interface OnChildLocationsChangedListener {
1439        public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
1440    }
1441
1442    static class AnimationEvent {
1443
1444        static AnimationFilter[] FILTERS = new AnimationFilter[] {
1445
1446                // ANIMATION_TYPE_ADD
1447                new AnimationFilter()
1448                        .animateAlpha()
1449                        .animateHeight()
1450                        .animateY()
1451                        .animateZ(),
1452
1453                // ANIMATION_TYPE_REMOVE
1454                new AnimationFilter()
1455                        .animateAlpha()
1456                        .animateHeight()
1457                        .animateY()
1458                        .animateZ(),
1459
1460                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
1461                new AnimationFilter()
1462                        .animateAlpha()
1463                        .animateHeight()
1464                        .animateY()
1465                        .animateZ(),
1466
1467                // ANIMATION_TYPE_TOP_PADDING_CHANGED
1468                new AnimationFilter()
1469                        .animateAlpha()
1470                        .animateHeight()
1471                        .animateY()
1472                        .animateDimmed()
1473                        .animateScale()
1474                        .animateZ(),
1475
1476                // ANIMATION_TYPE_START_DRAG
1477                new AnimationFilter()
1478                        .animateAlpha(),
1479
1480                // ANIMATION_TYPE_SNAP_BACK
1481                new AnimationFilter()
1482                        .animateAlpha(),
1483
1484                // ANIMATION_TYPE_ACTIVATED_CHILD
1485                new AnimationFilter()
1486                        .animateScale()
1487                        .animateAlpha(),
1488
1489                // ANIMATION_TYPE_DIMMED
1490                new AnimationFilter()
1491                        .animateY()
1492                        .animateScale()
1493                        .animateDimmed()
1494        };
1495
1496        static int[] LENGTHS = new int[] {
1497
1498                // ANIMATION_TYPE_ADD
1499                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1500
1501                // ANIMATION_TYPE_REMOVE
1502                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1503
1504                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
1505                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1506
1507                // ANIMATION_TYPE_TOP_PADDING_CHANGED
1508                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1509
1510                // ANIMATION_TYPE_START_DRAG
1511                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1512
1513                // ANIMATION_TYPE_SNAP_BACK
1514                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1515
1516                // ANIMATION_TYPE_ACTIVATED_CHILD
1517                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
1518
1519                // ANIMATION_TYPE_DIMMED
1520                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
1521        };
1522
1523        static int ANIMATION_TYPE_ADD = 0;
1524        static int ANIMATION_TYPE_REMOVE = 1;
1525        static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
1526        static int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
1527        static int ANIMATION_TYPE_START_DRAG = 4;
1528        static int ANIMATION_TYPE_SNAP_BACK = 5;
1529        static int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
1530        static int ANIMATION_TYPE_DIMMED = 7;
1531
1532        final long eventStartTime;
1533        final View changingView;
1534        final int animationType;
1535        final AnimationFilter filter;
1536        final long length;
1537
1538        AnimationEvent(View view, int type) {
1539            eventStartTime = AnimationUtils.currentAnimationTimeMillis();
1540            changingView = view;
1541            animationType = type;
1542            filter = FILTERS[type];
1543            length = LENGTHS[type];
1544        }
1545
1546        /**
1547         * Combines the length of several animation events into a single value.
1548         *
1549         * @param events The events of the lengths to combine.
1550         * @return The combined length. This is just the maximum of all length.
1551         */
1552        static long combineLength(ArrayList<AnimationEvent> events) {
1553            long length = 0;
1554            int size = events.size();
1555            for (int i = 0; i < size; i++) {
1556                length = Math.max(length, events.get(i).length);
1557            }
1558            return length;
1559        }
1560    }
1561
1562}
1563