NotificationStackScrollLayout.java revision 47c85a3525dcd0bbd3168632830e8ab491d18462
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;
21import android.graphics.Canvas;
22import android.graphics.Paint;
23import android.util.AttributeSet;
24import android.util.Log;
25import android.view.MotionEvent;
26import android.view.VelocityTracker;
27import android.view.View;
28import android.view.ViewConfiguration;
29import android.view.ViewGroup;
30import android.view.ViewTreeObserver;
31import android.view.animation.AnimationUtils;
32import android.widget.OverScroller;
33import com.android.systemui.ExpandHelper;
34import com.android.systemui.R;
35import com.android.systemui.SwipeHelper;
36import com.android.systemui.statusbar.ExpandableNotificationRow;
37import com.android.systemui.statusbar.ExpandableView;
38import com.android.systemui.statusbar.SpeedBumpView;
39import com.android.systemui.statusbar.policy.ScrollAdapter;
40import com.android.systemui.statusbar.stack.StackScrollState.ViewState;
41
42import java.util.ArrayList;
43
44/**
45 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
46 */
47public class NotificationStackScrollLayout extends ViewGroup
48        implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter,
49        ExpandableView.OnHeightChangedListener {
50
51    private static final String TAG = "NotificationStackScrollLayout";
52    private static final boolean DEBUG = false;
53    private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
54    private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f;
55    private static final float RUBBER_BAND_FACTOR_ON_PANEL_EXPAND = 0.21f;
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 ExpandHelper mExpandHelper;
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 mDownX;
79    private int mActivePointerId;
80
81    private int mSidePaddings;
82    private Paint mDebugPaint;
83    private int mContentHeight;
84    private int mCollapsedSize;
85    private int mBottomStackSlowDownHeight;
86    private int mBottomStackPeekSize;
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<View> mChildrenChangingPositions = new ArrayList<View>();
107    private ArrayList<AnimationEvent> mAnimationEvents
108            = new ArrayList<AnimationEvent>();
109    private ArrayList<View> mSwipedOutViews = new ArrayList<View>();
110    private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
111    private boolean mAnimationsEnabled;
112    private boolean mChangePositionInProgress;
113
114    /**
115     * The raw amount of the overScroll on the top, which is not rubber-banded.
116     */
117    private float mOverScrolledTopPixels;
118
119    /**
120     * The raw amount of the overScroll on the bottom, which is not rubber-banded.
121     */
122    private float mOverScrolledBottomPixels;
123
124    private OnChildLocationsChangedListener mListener;
125    private OnOverscrollTopChangedListener mOverscrollTopChangedListener;
126    private ExpandableView.OnHeightChangedListener mOnHeightChangedListener;
127    private boolean mNeedsAnimation;
128    private boolean mTopPaddingNeedsAnimation;
129    private boolean mDimmedNeedsAnimation;
130    private boolean mActivateNeedsAnimation;
131    private boolean mIsExpanded = true;
132    private boolean mChildrenUpdateRequested;
133    private SpeedBumpView mSpeedBumpView;
134    private boolean mIsExpansionChanging;
135    private boolean mExpandingNotification;
136    private boolean mExpandedInThisMotion;
137    private boolean mScrollingEnabled;
138
139    /**
140     * Was the scroller scrolled to the top when the down motion was observed?
141     */
142    private boolean mScrolledToTopOnFirstDown;
143
144    /**
145     * The minimal amount of over scroll which is needed in order to switch to the quick settings
146     * when over scrolling on a expanded card.
147     */
148    private float mMinTopOverScrollToEscape;
149    private int mIntrinsicPadding;
150    private int mNotificationTopPadding;
151    private int mMinStackHeight;
152    private boolean mDontReportNextOverScroll;
153
154    /**
155     * The maximum scrollPosition which we are allowed to reach when a notification was expanded.
156     * This is needed to avoid scrolling too far after the notification was collapsed in the same
157     * motion.
158     */
159    private int mMaxScrollAfterExpand;
160    private OnLongClickListener mLongClickListener;
161
162    /**
163     * Should in this touch motion only be scrolling allowed? It's true when the scroller was
164     * animating.
165     */
166    private boolean mOnlyScrollingInThisMotion;
167    private boolean mTouchEnabled = true;
168    private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
169            = new ViewTreeObserver.OnPreDrawListener() {
170        @Override
171        public boolean onPreDraw() {
172            updateChildren();
173            mChildrenUpdateRequested = false;
174            getViewTreeObserver().removeOnPreDrawListener(this);
175            return true;
176        }
177    };
178
179    public NotificationStackScrollLayout(Context context) {
180        this(context, null);
181    }
182
183    public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
184        this(context, attrs, 0);
185    }
186
187    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
188        this(context, attrs, defStyleAttr, 0);
189    }
190
191    public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
192            int defStyleRes) {
193        super(context, attrs, defStyleAttr, defStyleRes);
194        initView(context);
195        if (DEBUG) {
196            setWillNotDraw(false);
197            mDebugPaint = new Paint();
198            mDebugPaint.setColor(0xffff0000);
199            mDebugPaint.setStrokeWidth(2);
200            mDebugPaint.setStyle(Paint.Style.STROKE);
201        }
202    }
203
204    @Override
205    protected void onDraw(Canvas canvas) {
206        if (DEBUG) {
207            int y = mCollapsedSize;
208            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
209            y = (int) (getLayoutHeight() - mBottomStackPeekSize
210                    - mBottomStackSlowDownHeight);
211            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
212            y = (int) (getLayoutHeight() - mBottomStackPeekSize);
213            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
214            y = (int) getLayoutHeight();
215            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
216            y = getHeight() - getEmptyBottomMargin();
217            canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
218        }
219    }
220
221    private void initView(Context context) {
222        mScroller = new OverScroller(getContext());
223        setFocusable(true);
224        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
225        setClipChildren(false);
226        final ViewConfiguration configuration = ViewConfiguration.get(context);
227        mTouchSlop = configuration.getScaledTouchSlop();
228        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
229        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
230        mOverflingDistance = configuration.getScaledOverflingDistance();
231        float densityScale = getResources().getDisplayMetrics().density;
232        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
233        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
234        mSwipeHelper.setLongPressListener(mLongClickListener);
235
236        mSidePaddings = context.getResources()
237                .getDimensionPixelSize(R.dimen.notification_side_padding);
238        mCollapsedSize = context.getResources()
239                .getDimensionPixelSize(R.dimen.notification_min_height);
240        mBottomStackPeekSize = context.getResources()
241                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
242        mStackScrollAlgorithm = new StackScrollAlgorithm(context);
243        mPaddingBetweenElementsDimmed = context.getResources()
244                .getDimensionPixelSize(R.dimen.notification_padding_dimmed);
245        mPaddingBetweenElementsNormal = context.getResources()
246                .getDimensionPixelSize(R.dimen.notification_padding);
247        updatePadding(false);
248        int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
249        int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height);
250        mExpandHelper = new ExpandHelper(getContext(), this,
251                minHeight, maxHeight);
252        mExpandHelper.setEventSource(this);
253        mExpandHelper.setScrollAdapter(this);
254        mMinTopOverScrollToEscape = getResources().getDimensionPixelSize(
255                R.dimen.min_top_overscroll_to_qs);
256        mNotificationTopPadding = getResources().getDimensionPixelSize(
257                R.dimen.notifications_top_padding);
258        mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height);
259    }
260
261    private void updatePadding(boolean dimmed) {
262        mPaddingBetweenElements = dimmed
263                ? mPaddingBetweenElementsDimmed
264                : mPaddingBetweenElementsNormal;
265        mBottomStackSlowDownHeight = mStackScrollAlgorithm.getBottomStackSlowDownLength();
266        updateContentHeight();
267    }
268
269    @Override
270    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
271        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
272        int mode = MeasureSpec.getMode(widthMeasureSpec);
273        int size = MeasureSpec.getSize(widthMeasureSpec);
274        int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode);
275        measureChildren(childMeasureSpec, heightMeasureSpec);
276    }
277
278    @Override
279    protected void onLayout(boolean changed, int l, int t, int r, int b) {
280
281        // we layout all our children centered on the top
282        float centerX = getWidth() / 2.0f;
283        for (int i = 0; i < getChildCount(); i++) {
284            View child = getChildAt(i);
285            float width = child.getMeasuredWidth();
286            float height = child.getMeasuredHeight();
287            child.layout((int) (centerX - width / 2.0f),
288                    0,
289                    (int) (centerX + width / 2.0f),
290                    (int) height);
291        }
292        setMaxLayoutHeight(getHeight());
293        updateContentHeight();
294        updateScrollPositionIfNecessary();
295        requestChildrenUpdate();
296    }
297
298    public void updateSpeedBumpIndex(int newIndex) {
299        int currentIndex = indexOfChild(mSpeedBumpView);
300
301        // If we are currently layouted before the new speed bump index, we have to decrease it.
302        boolean validIndex = newIndex > 0;
303        if (newIndex > getChildCount() - 1) {
304            validIndex = false;
305            newIndex = -1;
306        }
307        if (validIndex && currentIndex != newIndex) {
308            changeViewPosition(mSpeedBumpView, newIndex);
309        }
310        updateSpeedBump(validIndex);
311        mAmbientState.setSpeedBumpIndex(newIndex);
312    }
313
314    public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) {
315        mListener = listener;
316    }
317
318    /**
319     * Returns the location the given child is currently rendered at.
320     *
321     * @param child the child to get the location for
322     * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants
323     */
324    public int getChildLocation(View child) {
325        ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child);
326        if (childViewState == null) {
327            return ViewState.LOCATION_UNKNOWN;
328        }
329        return childViewState.location;
330    }
331
332    private void setMaxLayoutHeight(int maxLayoutHeight) {
333        mMaxLayoutHeight = maxLayoutHeight;
334        updateAlgorithmHeightAndPadding();
335    }
336
337    private void updateAlgorithmHeightAndPadding() {
338        mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
339        mStackScrollAlgorithm.setTopPadding(mTopPadding);
340    }
341
342    /**
343     * @return whether the height of the layout needs to be adapted, in order to ensure that the
344     *         last child is not in the bottom stack.
345     */
346    private boolean needsHeightAdaption() {
347        return getNotGoneChildCount() > 1;
348    }
349
350    private boolean isViewExpanded(View view) {
351        if (view != null) {
352            ExpandableView expandView = (ExpandableView) view;
353            return expandView.getActualHeight() > mCollapsedSize;
354        }
355        return false;
356    }
357
358    /**
359     * Updates the children views according to the stack scroll algorithm. Call this whenever
360     * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
361     */
362    private void updateChildren() {
363        mAmbientState.setScrollY(mOwnScrollY);
364        mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState);
365        if (!isCurrentlyAnimating() && !mNeedsAnimation) {
366            applyCurrentState();
367        } else {
368            startAnimationToState();
369        }
370    }
371
372    private void requestChildrenUpdate() {
373        if (!mChildrenUpdateRequested) {
374            getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater);
375            mChildrenUpdateRequested = true;
376            invalidate();
377        }
378    }
379
380    private boolean isCurrentlyAnimating() {
381        return mStateAnimator.isRunning();
382    }
383
384    private void updateScrollPositionIfNecessary() {
385        int scrollRange = getScrollRange();
386        if (scrollRange < mOwnScrollY) {
387            mOwnScrollY = scrollRange;
388        }
389    }
390
391    public int getTopPadding() {
392        return mTopPadding;
393    }
394
395    private void setTopPadding(int topPadding, boolean animate) {
396        if (mTopPadding != topPadding) {
397            mTopPadding = topPadding;
398            updateAlgorithmHeightAndPadding();
399            updateContentHeight();
400            if (animate && mAnimationsEnabled && mIsExpanded) {
401                mTopPaddingNeedsAnimation = true;
402                mNeedsAnimation =  true;
403            }
404            requestChildrenUpdate();
405            if (mOnHeightChangedListener != null) {
406                mOnHeightChangedListener.onHeightChanged(null);
407            }
408        }
409    }
410
411    /**
412     * Update the height of the stack to a new height.
413     *
414     * @param height the new height of the stack
415     */
416    public void setStackHeight(float height) {
417        setIsExpanded(height > 0.0f);
418        int newStackHeight = (int) height;
419        int itemHeight = getItemHeight();
420        int bottomStackPeekSize = mBottomStackPeekSize;
421        int minStackHeight = itemHeight + bottomStackPeekSize;
422        int stackHeight;
423        if (newStackHeight - mTopPadding >= minStackHeight) {
424            setTranslationY(0);
425            stackHeight = newStackHeight;
426        } else {
427
428            // We did not reach the position yet where we actually start growing,
429            // so we translate the stack upwards.
430            int translationY = (newStackHeight - minStackHeight);
431            // A slight parallax effect is introduced in order for the stack to catch up with
432            // the top card.
433            float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight;
434            partiallyThere = Math.max(0, partiallyThere);
435            translationY += (1 - partiallyThere) * bottomStackPeekSize;
436            setTranslationY(translationY - mTopPadding);
437            stackHeight = (int) (height - (translationY - mTopPadding));
438        }
439        if (stackHeight != mCurrentStackHeight) {
440            mCurrentStackHeight = stackHeight;
441            updateAlgorithmHeightAndPadding();
442            requestChildrenUpdate();
443        }
444    }
445
446    /**
447     * Get the current height of the view. This is at most the msize of the view given by a the
448     * layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
449     *
450     * @return either the layout height or the externally defined height, whichever is smaller
451     */
452    private int getLayoutHeight() {
453        return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
454    }
455
456    public int getItemHeight() {
457        return mCollapsedSize;
458    }
459
460    public int getBottomStackPeekSize() {
461        return mBottomStackPeekSize;
462    }
463
464    public void setLongPressListener(View.OnLongClickListener listener) {
465        mSwipeHelper.setLongPressListener(listener);
466        mLongClickListener = listener;
467    }
468
469    public void onChildDismissed(View v) {
470        if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
471        final View veto = v.findViewById(R.id.veto);
472        if (veto != null && veto.getVisibility() != View.GONE) {
473            veto.performClick();
474        }
475        setSwipingInProgress(false);
476        if (mDragAnimPendingChildren.contains(v)) {
477            // We start the swipe and finish it in the same frame, we don't want any animation
478            // for the drag
479            mDragAnimPendingChildren.remove(v);
480        }
481        mSwipedOutViews.add(v);
482        mAmbientState.onDragFinished(v);
483    }
484
485    @Override
486    public void onChildSnappedBack(View animView) {
487        mAmbientState.onDragFinished(animView);
488        if (!mDragAnimPendingChildren.contains(animView)) {
489            if (mAnimationsEnabled) {
490                mSnappedBackChildren.add(animView);
491                mNeedsAnimation = true;
492            }
493            requestChildrenUpdate();
494        } else {
495            // We start the swipe and snap back in the same frame, we don't want any animation
496            mDragAnimPendingChildren.remove(animView);
497        }
498    }
499
500    @Override
501    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
502        return false;
503    }
504
505    public void onBeginDrag(View v) {
506        setSwipingInProgress(true);
507        mAmbientState.onBeginDrag(v);
508        if (mAnimationsEnabled) {
509            mDragAnimPendingChildren.add(v);
510            mNeedsAnimation = true;
511        }
512        requestChildrenUpdate();
513    }
514
515    public void onDragCancelled(View v) {
516        setSwipingInProgress(false);
517    }
518
519    public View getChildAtPosition(MotionEvent ev) {
520        return getChildAtPosition(ev.getX(), ev.getY());
521    }
522
523    public ExpandableView getChildAtRawPosition(float touchX, float touchY) {
524        int[] location = new int[2];
525        getLocationOnScreen(location);
526        return getChildAtPosition(touchX - location[0], touchY - location[1]);
527    }
528
529    public ExpandableView getChildAtPosition(float touchX, float touchY) {
530        // find the view under the pointer, accounting for GONE views
531        final int count = getChildCount();
532        for (int childIdx = 0; childIdx < count; childIdx++) {
533            ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx);
534            if (slidingChild.getVisibility() == GONE) {
535                continue;
536            }
537            float top = slidingChild.getTranslationY();
538            float bottom = top + slidingChild.getActualHeight();
539            int left = slidingChild.getLeft();
540            int right = slidingChild.getRight();
541
542            if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
543                return slidingChild;
544            }
545        }
546        return null;
547    }
548
549    public boolean canChildBeExpanded(View v) {
550        return v instanceof ExpandableNotificationRow
551                && ((ExpandableNotificationRow) v).isExpandable();
552    }
553
554    public void setUserExpandedChild(View v, boolean userExpanded) {
555        if (v instanceof ExpandableNotificationRow) {
556            ((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
557        }
558    }
559
560    public void setUserLockedChild(View v, boolean userLocked) {
561        if (v instanceof ExpandableNotificationRow) {
562            ((ExpandableNotificationRow) v).setUserLocked(userLocked);
563        }
564        removeLongPressCallback();
565        requestDisallowInterceptTouchEvent(true);
566    }
567
568    @Override
569    public void expansionStateChanged(boolean isExpanding) {
570        mExpandingNotification = isExpanding;
571        if (!mExpandedInThisMotion) {
572            mMaxScrollAfterExpand = mOwnScrollY;
573            mExpandedInThisMotion = true;
574        }
575    }
576
577    public void setScrollingEnabled(boolean enable) {
578        mScrollingEnabled = enable;
579    }
580
581    public void setExpandingEnabled(boolean enable) {
582        mExpandHelper.setEnabled(enable);
583    }
584
585    private boolean isScrollingEnabled() {
586        return mScrollingEnabled;
587    }
588
589    public View getChildContentView(View v) {
590        return v;
591    }
592
593    public boolean canChildBeDismissed(View v) {
594        final View veto = v.findViewById(R.id.veto);
595        return (veto != null && veto.getVisibility() != View.GONE);
596    }
597
598    private void setSwipingInProgress(boolean isSwiped) {
599        mSwipingInProgress = isSwiped;
600        if(isSwiped) {
601            requestDisallowInterceptTouchEvent(true);
602        }
603    }
604
605    @Override
606    protected void onConfigurationChanged(Configuration newConfig) {
607        super.onConfigurationChanged(newConfig);
608        float densityScale = getResources().getDisplayMetrics().density;
609        mSwipeHelper.setDensityScale(densityScale);
610        float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
611        mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
612        initView(getContext());
613    }
614
615    public void dismissRowAnimated(View child, int vel) {
616        mSwipeHelper.dismissChild(child, vel);
617    }
618
619    @Override
620    public boolean onTouchEvent(MotionEvent ev) {
621        if (!isEnabled()) {
622            return false;
623        }
624        boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL
625                || ev.getActionMasked()== MotionEvent.ACTION_UP;
626        boolean expandWantsIt = false;
627        if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) {
628            if (isCancelOrUp) {
629                mExpandHelper.onlyObserveMovements(false);
630            }
631            boolean wasExpandingBefore = mExpandingNotification;
632            expandWantsIt = mExpandHelper.onTouchEvent(ev);
633            if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore) {
634                dispatchDownEventToScroller(ev);
635            }
636        }
637        boolean scrollerWantsIt = false;
638        if (!mSwipingInProgress && !mExpandingNotification) {
639            scrollerWantsIt = onScrollTouch(ev);
640        }
641        boolean horizontalSwipeWantsIt = false;
642        if (!mIsBeingDragged
643                && !mExpandingNotification
644                && !mExpandedInThisMotion
645                && !mOnlyScrollingInThisMotion) {
646            horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
647        }
648        return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev);
649    }
650
651    private void dispatchDownEventToScroller(MotionEvent ev) {
652        MotionEvent downEvent = MotionEvent.obtain(ev);
653        downEvent.setAction(MotionEvent.ACTION_DOWN);
654        onScrollTouch(downEvent);
655        downEvent.recycle();
656    }
657
658    private boolean onScrollTouch(MotionEvent ev) {
659        if (!isScrollingEnabled()) {
660            return false;
661        }
662        initVelocityTrackerIfNotExists();
663        mVelocityTracker.addMovement(ev);
664
665        final int action = ev.getAction();
666
667        switch (action & MotionEvent.ACTION_MASK) {
668            case MotionEvent.ACTION_DOWN: {
669                if (getChildCount() == 0 || !isInContentBounds(ev)) {
670                    return false;
671                }
672                boolean isBeingDragged = !mScroller.isFinished();
673                setIsBeingDragged(isBeingDragged);
674
675                /*
676                 * If being flinged and user touches, stop the fling. isFinished
677                 * will be false if being flinged.
678                 */
679                if (!mScroller.isFinished()) {
680                    mScroller.forceFinished(true);
681                }
682
683                // Remember where the motion event started
684                mLastMotionY = (int) ev.getY();
685                mDownX = (int) ev.getX();
686                mActivePointerId = ev.getPointerId(0);
687                break;
688            }
689            case MotionEvent.ACTION_MOVE:
690                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
691                if (activePointerIndex == -1) {
692                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
693                    break;
694                }
695
696                final int y = (int) ev.getY(activePointerIndex);
697                final int x = (int) ev.getX(activePointerIndex);
698                int deltaY = mLastMotionY - y;
699                final int xDiff = Math.abs(x - mDownX);
700                final int yDiff = Math.abs(deltaY);
701                if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) {
702                    setIsBeingDragged(true);
703                    if (deltaY > 0) {
704                        deltaY -= mTouchSlop;
705                    } else {
706                        deltaY += mTouchSlop;
707                    }
708                }
709                if (mIsBeingDragged) {
710                    // Scroll to follow the motion event
711                    mLastMotionY = y;
712                    int range = getScrollRange();
713                    if (mExpandedInThisMotion) {
714                        range = Math.min(range, mMaxScrollAfterExpand);
715                    }
716
717                    float scrollAmount;
718                    if (deltaY < 0) {
719                        scrollAmount = overScrollDown(deltaY);
720                    } else {
721                        scrollAmount = overScrollUp(deltaY, range);
722                    }
723
724                    // Calling overScrollBy will call onOverScrolled, which
725                    // calls onScrollChanged if applicable.
726                    if (scrollAmount != 0.0f) {
727                        // The scrolling motion could not be compensated with the
728                        // existing overScroll, we have to scroll the view
729                        overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY,
730                                0, range, 0, getHeight() / 2, true);
731                    }
732                }
733                break;
734            case MotionEvent.ACTION_UP:
735                if (mIsBeingDragged) {
736                    final VelocityTracker velocityTracker = mVelocityTracker;
737                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
738                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
739
740                    if (shouldOverScrollFling(initialVelocity)) {
741                        onOverScrollFling(true, initialVelocity);
742                    } else {
743                        if (getChildCount() > 0) {
744                            if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
745                                float currentOverScrollTop = getCurrentOverScrollAmount(true);
746                                if (currentOverScrollTop == 0.0f || initialVelocity > 0) {
747                                    fling(-initialVelocity);
748                                } else {
749                                    onOverScrollFling(false, initialVelocity);
750                                }
751                            } else {
752                                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
753                                        getScrollRange())) {
754                                    postInvalidateOnAnimation();
755                                }
756                            }
757                        }
758
759                        mActivePointerId = INVALID_POINTER;
760                        endDrag();
761                    }
762                }
763                break;
764            case MotionEvent.ACTION_CANCEL:
765                if (mIsBeingDragged && getChildCount() > 0) {
766                    if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
767                        postInvalidateOnAnimation();
768                    }
769                    mActivePointerId = INVALID_POINTER;
770                    endDrag();
771                }
772                break;
773            case MotionEvent.ACTION_POINTER_DOWN: {
774                final int index = ev.getActionIndex();
775                mLastMotionY = (int) ev.getY(index);
776                mDownX = (int) ev.getX(index);
777                mActivePointerId = ev.getPointerId(index);
778                break;
779            }
780            case MotionEvent.ACTION_POINTER_UP:
781                onSecondaryPointerUp(ev);
782                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
783                mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
784                break;
785        }
786        return true;
787    }
788
789    private void onOverScrollFling(boolean open, int initialVelocity) {
790        if (mOverscrollTopChangedListener != null) {
791            mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open);
792        }
793        mDontReportNextOverScroll = true;
794        setOverScrollAmount(0.0f, true, false);
795    }
796
797    /**
798     * Perform a scroll upwards and adapt the overscroll amounts accordingly
799     *
800     * @param deltaY The amount to scroll upwards, has to be positive.
801     * @return The amount of scrolling to be performed by the scroller,
802     *         not handled by the overScroll amount.
803     */
804    private float overScrollUp(int deltaY, int range) {
805        deltaY = Math.max(deltaY, 0);
806        float currentTopAmount = getCurrentOverScrollAmount(true);
807        float newTopAmount = currentTopAmount - deltaY;
808        if (currentTopAmount > 0) {
809            setOverScrollAmount(newTopAmount, true /* onTop */,
810                    false /* animate */);
811        }
812        // Top overScroll might not grab all scrolling motion,
813        // we have to scroll as well.
814        float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f;
815        float newScrollY = mOwnScrollY + scrollAmount;
816        if (newScrollY > range) {
817            if (!mExpandedInThisMotion) {
818                float currentBottomPixels = getCurrentOverScrolledPixels(false);
819                // We overScroll on the top
820                setOverScrolledPixels(currentBottomPixels + newScrollY - range,
821                        false /* onTop */,
822                        false /* animate */);
823            }
824            mOwnScrollY = range;
825            scrollAmount = 0.0f;
826        }
827        return scrollAmount;
828    }
829
830    /**
831     * Perform a scroll downward and adapt the overscroll amounts accordingly
832     *
833     * @param deltaY The amount to scroll downwards, has to be negative.
834     * @return The amount of scrolling to be performed by the scroller,
835     *         not handled by the overScroll amount.
836     */
837    private float overScrollDown(int deltaY) {
838        deltaY = Math.min(deltaY, 0);
839        float currentBottomAmount = getCurrentOverScrollAmount(false);
840        float newBottomAmount = currentBottomAmount + deltaY;
841        if (currentBottomAmount > 0) {
842            setOverScrollAmount(newBottomAmount, false /* onTop */,
843                    false /* animate */);
844        }
845        // Bottom overScroll might not grab all scrolling motion,
846        // we have to scroll as well.
847        float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f;
848        float newScrollY = mOwnScrollY + scrollAmount;
849        if (newScrollY < 0) {
850            float currentTopPixels = getCurrentOverScrolledPixels(true);
851            // We overScroll on the top
852            setOverScrolledPixels(currentTopPixels - newScrollY,
853                    true /* onTop */,
854                    false /* animate */);
855            mOwnScrollY = 0;
856            scrollAmount = 0.0f;
857        }
858        return scrollAmount;
859    }
860
861    private void onSecondaryPointerUp(MotionEvent ev) {
862        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
863                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
864        final int pointerId = ev.getPointerId(pointerIndex);
865        if (pointerId == mActivePointerId) {
866            // This was our active pointer going up. Choose a new
867            // active pointer and adjust accordingly.
868            // TODO: Make this decision more intelligent.
869            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
870            mLastMotionY = (int) ev.getY(newPointerIndex);
871            mActivePointerId = ev.getPointerId(newPointerIndex);
872            if (mVelocityTracker != null) {
873                mVelocityTracker.clear();
874            }
875        }
876    }
877
878    private void initVelocityTrackerIfNotExists() {
879        if (mVelocityTracker == null) {
880            mVelocityTracker = VelocityTracker.obtain();
881        }
882    }
883
884    private void recycleVelocityTracker() {
885        if (mVelocityTracker != null) {
886            mVelocityTracker.recycle();
887            mVelocityTracker = null;
888        }
889    }
890
891    private void initOrResetVelocityTracker() {
892        if (mVelocityTracker == null) {
893            mVelocityTracker = VelocityTracker.obtain();
894        } else {
895            mVelocityTracker.clear();
896        }
897    }
898
899    @Override
900    public void computeScroll() {
901        if (mScroller.computeScrollOffset()) {
902            // This is called at drawing time by ViewGroup.
903            int oldX = mScrollX;
904            int oldY = mOwnScrollY;
905            int x = mScroller.getCurrX();
906            int y = mScroller.getCurrY();
907
908            if (oldX != x || oldY != y) {
909                final int range = getScrollRange();
910                if (y < 0 && oldY >= 0 || y > range && oldY <= range) {
911                    float currVelocity = mScroller.getCurrVelocity();
912                    if (currVelocity >= mMinimumVelocity) {
913                        mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance;
914                    }
915                }
916
917                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
918                        0, (int) (mMaxOverScroll), false);
919                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
920            }
921
922            // Keep on drawing until the animation has finished.
923            postInvalidateOnAnimation();
924        }
925    }
926
927    @Override
928    protected boolean overScrollBy(int deltaX, int deltaY,
929            int scrollX, int scrollY,
930            int scrollRangeX, int scrollRangeY,
931            int maxOverScrollX, int maxOverScrollY,
932            boolean isTouchEvent) {
933
934        int newScrollY = scrollY + deltaY;
935
936        final int top = -maxOverScrollY;
937        final int bottom = maxOverScrollY + scrollRangeY;
938
939        boolean clampedY = false;
940        if (newScrollY > bottom) {
941            newScrollY = bottom;
942            clampedY = true;
943        } else if (newScrollY < top) {
944            newScrollY = top;
945            clampedY = true;
946        }
947
948        onOverScrolled(0, newScrollY, false, clampedY);
949
950        return clampedY;
951    }
952
953    /**
954     * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded
955     * overscroll effect based on numPixels. By default this will also cancel animations on the
956     * same overScroll edge.
957     *
958     * @param numPixels The amount of pixels to overScroll by. These will be scaled according to
959     *                  the rubber-banding logic.
960     * @param onTop Should the effect be applied on top of the scroller.
961     * @param animate Should an animation be performed.
962     */
963    public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) {
964        setOverScrollAmount(numPixels * getRubberBandFactor(), onTop, animate, true);
965    }
966
967    /**
968     * Set the effective overScroll amount which will be directly reflected in the layout.
969     * By default this will also cancel animations on the same overScroll edge.
970     *
971     * @param amount The amount to overScroll by.
972     * @param onTop Should the effect be applied on top of the scroller.
973     * @param animate Should an animation be performed.
974     */
975    public void setOverScrollAmount(float amount, boolean onTop, boolean animate) {
976        setOverScrollAmount(amount, onTop, animate, true);
977    }
978
979    /**
980     * Set the effective overScroll amount which will be directly reflected in the layout.
981     *
982     * @param amount The amount to overScroll by.
983     * @param onTop Should the effect be applied on top of the scroller.
984     * @param animate Should an animation be performed.
985     * @param cancelAnimators Should running animations be cancelled.
986     */
987    public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
988            boolean cancelAnimators) {
989        if (cancelAnimators) {
990            mStateAnimator.cancelOverScrollAnimators(onTop);
991        }
992        setOverScrollAmountInternal(amount, onTop, animate);
993    }
994
995    private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate) {
996        amount = Math.max(0, amount);
997        if (animate) {
998            mStateAnimator.animateOverScrollToAmount(amount, onTop);
999        } else {
1000            setOverScrolledPixels(amount / getRubberBandFactor(), onTop);
1001            mAmbientState.setOverScrollAmount(amount, onTop);
1002            if (onTop) {
1003                notifyOverscrollTopListener(amount);
1004            }
1005            requestChildrenUpdate();
1006        }
1007    }
1008
1009    private void notifyOverscrollTopListener(float amount) {
1010        mExpandHelper.onlyObserveMovements(amount > 1.0f);
1011        if (mDontReportNextOverScroll) {
1012            mDontReportNextOverScroll = false;
1013            return;
1014        }
1015        if (mOverscrollTopChangedListener != null) {
1016            mOverscrollTopChangedListener.onOverscrollTopChanged(amount);
1017        }
1018    }
1019
1020    public void setOverscrollTopChangedListener(
1021            OnOverscrollTopChangedListener overscrollTopChangedListener) {
1022        mOverscrollTopChangedListener = overscrollTopChangedListener;
1023    }
1024
1025    public float getCurrentOverScrollAmount(boolean top) {
1026        return mAmbientState.getOverScrollAmount(top);
1027    }
1028
1029    public float getCurrentOverScrolledPixels(boolean top) {
1030        return top? mOverScrolledTopPixels : mOverScrolledBottomPixels;
1031    }
1032
1033    private void setOverScrolledPixels(float amount, boolean onTop) {
1034        if (onTop) {
1035            mOverScrolledTopPixels = amount;
1036        } else {
1037            mOverScrolledBottomPixels = amount;
1038        }
1039    }
1040
1041    private void customScrollTo(int y) {
1042        mOwnScrollY = y;
1043        updateChildren();
1044    }
1045
1046    @Override
1047    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
1048        // Treat animating scrolls differently; see #computeScroll() for why.
1049        if (!mScroller.isFinished()) {
1050            final int oldX = mScrollX;
1051            final int oldY = mOwnScrollY;
1052            mScrollX = scrollX;
1053            mOwnScrollY = scrollY;
1054            if (clampedY) {
1055                springBack();
1056            } else {
1057                onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
1058                invalidateParentIfNeeded();
1059                updateChildren();
1060                float overScrollTop = getCurrentOverScrollAmount(true);
1061                if (mOwnScrollY < 0) {
1062                    notifyOverscrollTopListener(-mOwnScrollY);
1063                } else {
1064                    notifyOverscrollTopListener(overScrollTop);
1065                }
1066            }
1067        } else {
1068            customScrollTo(scrollY);
1069            scrollTo(scrollX, mScrollY);
1070        }
1071    }
1072
1073    private void springBack() {
1074        int scrollRange = getScrollRange();
1075        boolean overScrolledTop = mOwnScrollY <= 0;
1076        boolean overScrolledBottom = mOwnScrollY >= scrollRange;
1077        if (overScrolledTop || overScrolledBottom) {
1078            boolean onTop;
1079            float newAmount;
1080            if (overScrolledTop) {
1081                onTop = true;
1082                newAmount = -mOwnScrollY;
1083                mOwnScrollY = 0;
1084                mDontReportNextOverScroll = true;
1085            } else {
1086                onTop = false;
1087                newAmount = mOwnScrollY - scrollRange;
1088                mOwnScrollY = scrollRange;
1089            }
1090            setOverScrollAmount(newAmount, onTop, false);
1091            setOverScrollAmount(0.0f, onTop, true);
1092            mScroller.forceFinished(true);
1093        }
1094    }
1095
1096    private int getScrollRange() {
1097        int scrollRange = 0;
1098        ExpandableView firstChild = (ExpandableView) getFirstChildNotGone();
1099        if (firstChild != null) {
1100            int contentHeight = getContentHeight();
1101            int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild);
1102            scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize
1103                    + mBottomStackSlowDownHeight);
1104            if (scrollRange > 0) {
1105                View lastChild = getLastChildNotGone();
1106                // We want to at least be able collapse the first item and not ending in a weird
1107                // end state.
1108                scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize);
1109            }
1110        }
1111        return scrollRange;
1112    }
1113
1114    /**
1115     * @return the first child which has visibility unequal to GONE
1116     */
1117    private View getFirstChildNotGone() {
1118        int childCount = getChildCount();
1119        for (int i = 0; i < childCount; i++) {
1120            View child = getChildAt(i);
1121            if (child.getVisibility() != View.GONE) {
1122                return child;
1123            }
1124        }
1125        return null;
1126    }
1127
1128    /**
1129     * @return The first child which has visibility unequal to GONE which is currently below the
1130     *         given translationY or equal to it.
1131     */
1132    private View getFirstChildBelowTranlsationY(float translationY) {
1133        int childCount = getChildCount();
1134        for (int i = 0; i < childCount; i++) {
1135            View child = getChildAt(i);
1136            if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) {
1137                return child;
1138            }
1139        }
1140        return null;
1141    }
1142
1143    /**
1144     * @return the last child which has visibility unequal to GONE
1145     */
1146    public View getLastChildNotGone() {
1147        int childCount = getChildCount();
1148        for (int i = childCount - 1; i >= 0; i--) {
1149            View child = getChildAt(i);
1150            if (child.getVisibility() != View.GONE) {
1151                return child;
1152            }
1153        }
1154        return null;
1155    }
1156
1157    /**
1158     * @return the number of children which have visibility unequal to GONE
1159     */
1160    public int getNotGoneChildCount() {
1161        int childCount = getChildCount();
1162        int count = 0;
1163        for (int i = 0; i < childCount; i++) {
1164            View child = getChildAt(i);
1165            if (child.getVisibility() != View.GONE) {
1166                count++;
1167            }
1168        }
1169        return count;
1170    }
1171
1172    private int getMaxExpandHeight(View view) {
1173        if (view instanceof ExpandableNotificationRow) {
1174            ExpandableNotificationRow row = (ExpandableNotificationRow) view;
1175            return row.getIntrinsicHeight();
1176        }
1177        return view.getHeight();
1178    }
1179
1180    private int getContentHeight() {
1181        return mContentHeight;
1182    }
1183
1184    private void updateContentHeight() {
1185        int height = 0;
1186        for (int i = 0; i < getChildCount(); i++) {
1187            View child = getChildAt(i);
1188            if (child.getVisibility() != View.GONE) {
1189                if (height != 0) {
1190                    // add the padding before this element
1191                    height += mPaddingBetweenElements;
1192                }
1193                if (child instanceof ExpandableNotificationRow) {
1194                    ExpandableNotificationRow row = (ExpandableNotificationRow) child;
1195                    height += row.getIntrinsicHeight();
1196                } else if (child instanceof ExpandableView) {
1197                    ExpandableView expandableView = (ExpandableView) child;
1198                    height += expandableView.getActualHeight();
1199                }
1200            }
1201        }
1202        mContentHeight = height + mTopPadding;
1203    }
1204
1205    /**
1206     * Fling the scroll view
1207     *
1208     * @param velocityY The initial velocity in the Y direction. Positive
1209     *                  numbers mean that the finger/cursor is moving down the screen,
1210     *                  which means we want to scroll towards the top.
1211     */
1212    private void fling(int velocityY) {
1213        if (getChildCount() > 0) {
1214            int scrollRange = getScrollRange();
1215
1216            float topAmount = getCurrentOverScrollAmount(true);
1217            float bottomAmount = getCurrentOverScrollAmount(false);
1218            if (velocityY < 0 && topAmount > 0) {
1219                mOwnScrollY -= (int) topAmount;
1220                mDontReportNextOverScroll = true;
1221                setOverScrollAmount(0, true, false);
1222                mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor()
1223                        * mOverflingDistance + topAmount;
1224            } else if (velocityY > 0 && bottomAmount > 0) {
1225                mOwnScrollY += bottomAmount;
1226                setOverScrollAmount(0, false, false);
1227                mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor()
1228                        * mOverflingDistance + bottomAmount;
1229            } else {
1230                // it will be set once we reach the boundary
1231                mMaxOverScroll = 0.0f;
1232            }
1233            mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0,
1234                    Math.max(0, scrollRange), 0, Integer.MAX_VALUE / 2);
1235
1236            postInvalidateOnAnimation();
1237        }
1238    }
1239
1240    /**
1241     * @return Whether a fling performed on the top overscroll edge lead to the expanded
1242     * overScroll view (i.e QS).
1243     */
1244    private boolean shouldOverScrollFling(int initialVelocity) {
1245        float topOverScroll = getCurrentOverScrollAmount(true);
1246        return mScrolledToTopOnFirstDown
1247                && !mExpandedInThisMotion
1248                && topOverScroll > mMinTopOverScrollToEscape
1249                && initialVelocity > 0;
1250    }
1251
1252    public void updateTopPadding(float qsHeight, int scrollY, boolean animate) {
1253        float start = qsHeight - scrollY + mNotificationTopPadding;
1254        float stackHeight = getHeight() - start;
1255        if (stackHeight <= mMinStackHeight) {
1256            float overflow = mMinStackHeight - stackHeight;
1257            stackHeight = mMinStackHeight;
1258            start = getHeight() - stackHeight;
1259            setTranslationY(overflow);
1260        } else {
1261            setTranslationY(0);
1262        }
1263        setTopPadding(clampPadding((int) start), animate);
1264    }
1265
1266    private int clampPadding(int desiredPadding) {
1267        return Math.max(desiredPadding, mIntrinsicPadding);
1268    }
1269
1270    private float getRubberBandFactor() {
1271        if (mExpandedInThisMotion) {
1272            return RUBBER_BAND_FACTOR_AFTER_EXPAND;
1273        } else if (mIsExpansionChanging) {
1274            return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND;
1275        } else if (mScrolledToTopOnFirstDown) {
1276            return 1.0f;
1277        }
1278        return RUBBER_BAND_FACTOR_NORMAL;
1279    }
1280
1281    private void endDrag() {
1282        setIsBeingDragged(false);
1283
1284        recycleVelocityTracker();
1285
1286        if (getCurrentOverScrollAmount(true /* onTop */) > 0) {
1287            setOverScrollAmount(0, true /* onTop */, true /* animate */);
1288        }
1289        if (getCurrentOverScrollAmount(false /* onTop */) > 0) {
1290            setOverScrollAmount(0, false /* onTop */, true /* animate */);
1291        }
1292    }
1293
1294    @Override
1295    public boolean onInterceptTouchEvent(MotionEvent ev) {
1296        initDownStates(ev);
1297        boolean expandWantsIt = false;
1298        if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) {
1299            expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev);
1300        }
1301        boolean scrollWantsIt = false;
1302        if (!mSwipingInProgress && !mExpandingNotification) {
1303            scrollWantsIt = onInterceptTouchEventScroll(ev);
1304        }
1305        boolean swipeWantsIt = false;
1306        if (!mIsBeingDragged
1307                && !mExpandingNotification
1308                && !mExpandedInThisMotion
1309                && !mOnlyScrollingInThisMotion) {
1310            swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
1311        }
1312        return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev);
1313    }
1314
1315    private void initDownStates(MotionEvent ev) {
1316        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
1317            mExpandedInThisMotion = false;
1318            mOnlyScrollingInThisMotion = !mScroller.isFinished();
1319        }
1320    }
1321
1322    @Override
1323    protected void onViewRemoved(View child) {
1324        super.onViewRemoved(child);
1325        mStackScrollAlgorithm.notifyChildrenChanged(this);
1326        if (mChangePositionInProgress) {
1327            // This is only a position change, don't do anything special
1328            return;
1329        }
1330        ((ExpandableView) child).setOnHeightChangedListener(null);
1331        mCurrentStackScrollState.removeViewStateForView(child);
1332        updateScrollStateForRemovedChild(child);
1333        boolean animationGenerated = generateRemoveAnimation(child);
1334        if (animationGenerated && !mSwipedOutViews.contains(child)) {
1335            // Add this view to an overlay in order to ensure that it will still be temporary
1336            // drawn when removed
1337            getOverlay().add(child);
1338        }
1339    }
1340
1341    /**
1342     * Generate a remove animation for a child view.
1343     *
1344     * @param child The view to generate the remove animation for.
1345     * @return Whether an animation was generated.
1346     */
1347    private boolean generateRemoveAnimation(View child) {
1348        if (mIsExpanded && mAnimationsEnabled) {
1349            if (!mChildrenToAddAnimated.contains(child)) {
1350                // Generate Animations
1351                mChildrenToRemoveAnimated.add(child);
1352                mNeedsAnimation = true;
1353                return true;
1354            } else {
1355                mChildrenToAddAnimated.remove(child);
1356                return false;
1357            }
1358        }
1359        return false;
1360    }
1361
1362    /**
1363     * Updates the scroll position when a child was removed
1364     *
1365     * @param removedChild the removed child
1366     */
1367    private void updateScrollStateForRemovedChild(View removedChild) {
1368        int startingPosition = getPositionInLinearLayout(removedChild);
1369        int childHeight = removedChild.getHeight() + mPaddingBetweenElements;
1370        int endPosition = startingPosition + childHeight;
1371        if (endPosition <= mOwnScrollY) {
1372            // This child is fully scrolled of the top, so we have to deduct its height from the
1373            // scrollPosition
1374            mOwnScrollY -= childHeight;
1375        } else if (startingPosition < mOwnScrollY) {
1376            // This child is currently being scrolled into, set the scroll position to the start of
1377            // this child
1378            mOwnScrollY = startingPosition;
1379        }
1380    }
1381
1382    private int getPositionInLinearLayout(View requestedChild) {
1383        int position = 0;
1384        for (int i = 0; i < getChildCount(); i++) {
1385            View child = getChildAt(i);
1386            if (child == requestedChild) {
1387                return position;
1388            }
1389            if (child.getVisibility() != View.GONE) {
1390                position += child.getHeight();
1391                if (i < getChildCount()-1) {
1392                    position += mPaddingBetweenElements;
1393                }
1394            }
1395        }
1396        return 0;
1397    }
1398
1399    @Override
1400    protected void onViewAdded(View child) {
1401        super.onViewAdded(child);
1402        mStackScrollAlgorithm.notifyChildrenChanged(this);
1403        ((ExpandableView) child).setOnHeightChangedListener(this);
1404        generateAddAnimation(child);
1405    }
1406
1407    public void setAnimationsEnabled(boolean animationsEnabled) {
1408        mAnimationsEnabled = animationsEnabled;
1409    }
1410
1411    public boolean isAddOrRemoveAnimationPending() {
1412        return mNeedsAnimation
1413                && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty());
1414    }
1415    /**
1416     * Generate an animation for an added child view.
1417     *
1418     * @param child The view to be added.
1419     */
1420    public void generateAddAnimation(View child) {
1421        if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) {
1422            // Generate Animations
1423            mChildrenToAddAnimated.add(child);
1424            mNeedsAnimation = true;
1425        }
1426    }
1427
1428    /**
1429     * Change the position of child to a new location
1430     *
1431     * @param child the view to change the position for
1432     * @param newIndex the new index
1433     */
1434    public void changeViewPosition(View child, int newIndex) {
1435        if (child != null && child.getParent() == this) {
1436            mChangePositionInProgress = true;
1437            removeView(child);
1438            addView(child, newIndex);
1439            mChangePositionInProgress = false;
1440            if (mIsExpanded && mAnimationsEnabled) {
1441                mChildrenChangingPositions.add(child);
1442                mNeedsAnimation = true;
1443            }
1444        }
1445    }
1446
1447    private void startAnimationToState() {
1448        if (mNeedsAnimation) {
1449            generateChildHierarchyEvents();
1450            mNeedsAnimation = false;
1451        }
1452        if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) {
1453            mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState);
1454            mAnimationEvents.clear();
1455        } else {
1456            applyCurrentState();
1457        }
1458    }
1459
1460    private void generateChildHierarchyEvents() {
1461        generateChildRemovalEvents();
1462        generateChildAdditionEvents();
1463        generatePositionChangeEvents();
1464        generateSnapBackEvents();
1465        generateDragEvents();
1466        generateTopPaddingEvent();
1467        generateActivateEvent();
1468        generateDimmedEvent();
1469        mNeedsAnimation = false;
1470    }
1471
1472    private void generateSnapBackEvents() {
1473        for (View child : mSnappedBackChildren) {
1474            mAnimationEvents.add(new AnimationEvent(child,
1475                    AnimationEvent.ANIMATION_TYPE_SNAP_BACK));
1476        }
1477        mSnappedBackChildren.clear();
1478    }
1479
1480    private void generateDragEvents() {
1481        for (View child : mDragAnimPendingChildren) {
1482            mAnimationEvents.add(new AnimationEvent(child,
1483                    AnimationEvent.ANIMATION_TYPE_START_DRAG));
1484        }
1485        mDragAnimPendingChildren.clear();
1486    }
1487
1488    private void generateChildRemovalEvents() {
1489        for (View child : mChildrenToRemoveAnimated) {
1490            boolean childWasSwipedOut = mSwipedOutViews.contains(child);
1491            int animationType = childWasSwipedOut
1492                    ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT
1493                    : AnimationEvent.ANIMATION_TYPE_REMOVE;
1494            AnimationEvent event = new AnimationEvent(child, animationType);
1495
1496            // we need to know the view after this one
1497            event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY());
1498            mAnimationEvents.add(event);
1499        }
1500        mSwipedOutViews.clear();
1501        mChildrenToRemoveAnimated.clear();
1502    }
1503
1504    private void generatePositionChangeEvents() {
1505        for (View child : mChildrenChangingPositions) {
1506            mAnimationEvents.add(new AnimationEvent(child,
1507                    AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION));
1508        }
1509        mChildrenChangingPositions.clear();
1510    }
1511
1512    private void generateChildAdditionEvents() {
1513        for (View child : mChildrenToAddAnimated) {
1514            mAnimationEvents.add(new AnimationEvent(child,
1515                    AnimationEvent.ANIMATION_TYPE_ADD));
1516        }
1517        mChildrenToAddAnimated.clear();
1518    }
1519
1520    private void generateTopPaddingEvent() {
1521        if (mTopPaddingNeedsAnimation) {
1522            mAnimationEvents.add(
1523                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED));
1524        }
1525        mTopPaddingNeedsAnimation = false;
1526    }
1527
1528    private void generateActivateEvent() {
1529        if (mActivateNeedsAnimation) {
1530            mAnimationEvents.add(
1531                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD));
1532        }
1533        mActivateNeedsAnimation = false;
1534    }
1535
1536    private void generateDimmedEvent() {
1537        if (mDimmedNeedsAnimation) {
1538            mAnimationEvents.add(
1539                    new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED));
1540        }
1541        mDimmedNeedsAnimation = false;
1542    }
1543
1544    private boolean onInterceptTouchEventScroll(MotionEvent ev) {
1545        if (!isScrollingEnabled()) {
1546            return false;
1547        }
1548        /*
1549         * This method JUST determines whether we want to intercept the motion.
1550         * If we return true, onMotionEvent will be called and we do the actual
1551         * scrolling there.
1552         */
1553
1554        /*
1555        * Shortcut the most recurring case: the user is in the dragging
1556        * state and he is moving his finger.  We want to intercept this
1557        * motion.
1558        */
1559        final int action = ev.getAction();
1560        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
1561            return true;
1562        }
1563
1564        switch (action & MotionEvent.ACTION_MASK) {
1565            case MotionEvent.ACTION_MOVE: {
1566                /*
1567                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
1568                 * whether the user has moved far enough from his original down touch.
1569                 */
1570
1571                /*
1572                * Locally do absolute value. mLastMotionY is set to the y value
1573                * of the down event.
1574                */
1575                final int activePointerId = mActivePointerId;
1576                if (activePointerId == INVALID_POINTER) {
1577                    // If we don't have a valid id, the touch down wasn't on content.
1578                    break;
1579                }
1580
1581                final int pointerIndex = ev.findPointerIndex(activePointerId);
1582                if (pointerIndex == -1) {
1583                    Log.e(TAG, "Invalid pointerId=" + activePointerId
1584                            + " in onInterceptTouchEvent");
1585                    break;
1586                }
1587
1588                final int y = (int) ev.getY(pointerIndex);
1589                final int x = (int) ev.getX(pointerIndex);
1590                final int yDiff = Math.abs(y - mLastMotionY);
1591                final int xDiff = Math.abs(x - mDownX);
1592                if (yDiff > mTouchSlop && yDiff > xDiff) {
1593                    setIsBeingDragged(true);
1594                    mLastMotionY = y;
1595                    mDownX = x;
1596                    initVelocityTrackerIfNotExists();
1597                    mVelocityTracker.addMovement(ev);
1598                }
1599                break;
1600            }
1601
1602            case MotionEvent.ACTION_DOWN: {
1603                final int y = (int) ev.getY();
1604                if (getChildAtPosition(ev.getX(), y) == null) {
1605                    setIsBeingDragged(false);
1606                    recycleVelocityTracker();
1607                    break;
1608                }
1609
1610                /*
1611                 * Remember location of down touch.
1612                 * ACTION_DOWN always refers to pointer index 0.
1613                 */
1614                mLastMotionY = y;
1615                mDownX = (int) ev.getX();
1616                mActivePointerId = ev.getPointerId(0);
1617                mScrolledToTopOnFirstDown = isScrolledToTop();
1618
1619                initOrResetVelocityTracker();
1620                mVelocityTracker.addMovement(ev);
1621                /*
1622                * If being flinged and user touches the screen, initiate drag;
1623                * otherwise don't.  mScroller.isFinished should be false when
1624                * being flinged.
1625                */
1626                boolean isBeingDragged = !mScroller.isFinished();
1627                setIsBeingDragged(isBeingDragged);
1628                break;
1629            }
1630
1631            case MotionEvent.ACTION_CANCEL:
1632            case MotionEvent.ACTION_UP:
1633                /* Release the drag */
1634                setIsBeingDragged(false);
1635                mActivePointerId = INVALID_POINTER;
1636                recycleVelocityTracker();
1637                if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
1638                    postInvalidateOnAnimation();
1639                }
1640                break;
1641            case MotionEvent.ACTION_POINTER_UP:
1642                onSecondaryPointerUp(ev);
1643                break;
1644        }
1645
1646        /*
1647        * The only time we want to intercept motion events is if we are in the
1648        * drag mode.
1649        */
1650        return mIsBeingDragged;
1651    }
1652
1653    /**
1654     * @return Whether the specified motion event is actually happening over the content.
1655     */
1656    private boolean isInContentBounds(MotionEvent event) {
1657        return event.getY() < getHeight() - getEmptyBottomMargin();
1658    }
1659
1660    private void setIsBeingDragged(boolean isDragged) {
1661        mIsBeingDragged = isDragged;
1662        if (isDragged) {
1663            requestDisallowInterceptTouchEvent(true);
1664            removeLongPressCallback();
1665        }
1666    }
1667
1668    @Override
1669    public void onWindowFocusChanged(boolean hasWindowFocus) {
1670        super.onWindowFocusChanged(hasWindowFocus);
1671        if (!hasWindowFocus) {
1672            removeLongPressCallback();
1673        }
1674    }
1675
1676    public void removeLongPressCallback() {
1677        mSwipeHelper.removeLongPressCallback();
1678    }
1679
1680    @Override
1681    public boolean isScrolledToTop() {
1682        return mOwnScrollY == 0;
1683    }
1684
1685    @Override
1686    public boolean isScrolledToBottom() {
1687        return mOwnScrollY >= getScrollRange();
1688    }
1689
1690    @Override
1691    public View getHostView() {
1692        return this;
1693    }
1694
1695    public int getEmptyBottomMargin() {
1696        int emptyMargin = mMaxLayoutHeight - mContentHeight;
1697        if (needsHeightAdaption()) {
1698            emptyMargin = emptyMargin - mBottomStackSlowDownHeight - mBottomStackPeekSize;
1699        } else {
1700            emptyMargin = emptyMargin - mBottomStackPeekSize;
1701        }
1702        return Math.max(emptyMargin, 0);
1703    }
1704
1705    public void onExpansionStarted() {
1706        mIsExpansionChanging = true;
1707        mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState);
1708    }
1709
1710    public void onExpansionStopped() {
1711        mIsExpansionChanging = false;
1712        mStackScrollAlgorithm.onExpansionStopped();
1713    }
1714
1715    private void setIsExpanded(boolean isExpanded) {
1716        mIsExpanded = isExpanded;
1717        mStackScrollAlgorithm.setIsExpanded(isExpanded);
1718        if (!isExpanded) {
1719            mOwnScrollY = 0;
1720            mSpeedBumpView.collapse();
1721        }
1722    }
1723
1724    @Override
1725    public void onHeightChanged(ExpandableView view) {
1726        updateContentHeight();
1727        updateScrollPositionIfNecessary();
1728        if (mOnHeightChangedListener != null) {
1729            mOnHeightChangedListener.onHeightChanged(view);
1730        }
1731        requestChildrenUpdate();
1732    }
1733
1734    public void setOnHeightChangedListener(
1735            ExpandableView.OnHeightChangedListener mOnHeightChangedListener) {
1736        this.mOnHeightChangedListener = mOnHeightChangedListener;
1737    }
1738
1739    public void onChildAnimationFinished() {
1740        requestChildrenUpdate();
1741    }
1742
1743    /**
1744     * See {@link AmbientState#setDimmed}.
1745     */
1746    public void setDimmed(boolean dimmed, boolean animate) {
1747        mStackScrollAlgorithm.setDimmed(dimmed);
1748        mAmbientState.setDimmed(dimmed);
1749        updatePadding(dimmed);
1750        if (animate && mAnimationsEnabled) {
1751            mDimmedNeedsAnimation = true;
1752            mNeedsAnimation =  true;
1753        }
1754        requestChildrenUpdate();
1755    }
1756
1757    /**
1758     * See {@link AmbientState#setActivatedChild}.
1759     */
1760    public void setActivatedChild(View activatedChild) {
1761        mAmbientState.setActivatedChild(activatedChild);
1762        if (mAnimationsEnabled) {
1763            mActivateNeedsAnimation = true;
1764            mNeedsAnimation =  true;
1765        }
1766        requestChildrenUpdate();
1767    }
1768
1769    public View getActivatedChild() {
1770        return mAmbientState.getActivatedChild();
1771    }
1772
1773    private void applyCurrentState() {
1774        mCurrentStackScrollState.apply();
1775        if (mListener != null) {
1776            mListener.onChildLocationsChanged(this);
1777        }
1778    }
1779
1780    public void setSpeedBumpView(SpeedBumpView speedBumpView) {
1781        mSpeedBumpView = speedBumpView;
1782        addView(speedBumpView);
1783    }
1784
1785    private void updateSpeedBump(boolean visible) {
1786        boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE;
1787        if (visible != notGoneBefore) {
1788            int newVisibility = visible ? VISIBLE : GONE;
1789            mSpeedBumpView.setVisibility(newVisibility);
1790            if (visible) {
1791                mSpeedBumpView.collapse();
1792                // Make invisible to ensure that the appear animation is played.
1793                mSpeedBumpView.setInvisible();
1794                if (!mIsExpansionChanging) {
1795                    generateAddAnimation(mSpeedBumpView);
1796                }
1797            } else {
1798                mSpeedBumpView.performVisibilityAnimation(false);
1799                generateRemoveAnimation(mSpeedBumpView);
1800            }
1801        }
1802    }
1803
1804    public void goToFullShade() {
1805        updateSpeedBump(true);
1806    }
1807
1808    public void cancelExpandHelper() {
1809        mExpandHelper.cancel();
1810    }
1811
1812    public void setIntrinsicPadding(int intrinsicPadding) {
1813        mIntrinsicPadding = intrinsicPadding;
1814    }
1815
1816    /**
1817     * @return the y position of the first notification
1818     */
1819    public float getNotificationsTopY() {
1820        return mTopPadding + getTranslationY();
1821    }
1822
1823    public void setTouchEnabled(boolean touchEnabled) {
1824        mTouchEnabled = touchEnabled;
1825    }
1826
1827    @Override
1828    public boolean dispatchTouchEvent(MotionEvent ev) {
1829        if (!mTouchEnabled) {
1830            return false;
1831        }
1832        return super.dispatchTouchEvent(ev);
1833    }
1834
1835    /**
1836     * A listener that is notified when some child locations might have changed.
1837     */
1838    public interface OnChildLocationsChangedListener {
1839        public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout);
1840    }
1841
1842    /**
1843     * A listener that gets notified when the overscroll at the top has changed.
1844     */
1845    public interface OnOverscrollTopChangedListener {
1846        public void onOverscrollTopChanged(float amount);
1847
1848        /**
1849         * Notify a listener that the scroller wants to escape from the scrolling motion and
1850         * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS)
1851         *
1852         * @param velocity The velocity that the Scroller had when over flinging
1853         * @param open Should the fling open or close the overscroll view.
1854         */
1855        public void flingTopOverscroll(float velocity, boolean open);
1856    }
1857
1858    static class AnimationEvent {
1859
1860        static AnimationFilter[] FILTERS = new AnimationFilter[] {
1861
1862                // ANIMATION_TYPE_ADD
1863                new AnimationFilter()
1864                        .animateAlpha()
1865                        .animateHeight()
1866                        .animateTopInset()
1867                        .animateY()
1868                        .animateZ()
1869                        .hasDelays(),
1870
1871                // ANIMATION_TYPE_REMOVE
1872                new AnimationFilter()
1873                        .animateAlpha()
1874                        .animateHeight()
1875                        .animateTopInset()
1876                        .animateY()
1877                        .animateZ()
1878                        .hasDelays(),
1879
1880                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
1881                new AnimationFilter()
1882                        .animateAlpha()
1883                        .animateHeight()
1884                        .animateTopInset()
1885                        .animateY()
1886                        .animateZ()
1887                        .hasDelays(),
1888
1889                // ANIMATION_TYPE_TOP_PADDING_CHANGED
1890                new AnimationFilter()
1891                        .animateAlpha()
1892                        .animateHeight()
1893                        .animateTopInset()
1894                        .animateY()
1895                        .animateDimmed()
1896                        .animateScale()
1897                        .animateZ(),
1898
1899                // ANIMATION_TYPE_START_DRAG
1900                new AnimationFilter()
1901                        .animateAlpha(),
1902
1903                // ANIMATION_TYPE_SNAP_BACK
1904                new AnimationFilter()
1905                        .animateAlpha(),
1906
1907                // ANIMATION_TYPE_ACTIVATED_CHILD
1908                new AnimationFilter()
1909                        .animateScale()
1910                        .animateAlpha(),
1911
1912                // ANIMATION_TYPE_DIMMED
1913                new AnimationFilter()
1914                        .animateY()
1915                        .animateScale()
1916                        .animateDimmed(),
1917
1918                // ANIMATION_TYPE_CHANGE_POSITION
1919                new AnimationFilter()
1920                        .animateAlpha()
1921                        .animateHeight()
1922                        .animateTopInset()
1923                        .animateY()
1924                        .animateZ()
1925        };
1926
1927        static int[] LENGTHS = new int[] {
1928
1929                // ANIMATION_TYPE_ADD
1930                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
1931
1932                // ANIMATION_TYPE_REMOVE
1933                StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR,
1934
1935                // ANIMATION_TYPE_REMOVE_SWIPED_OUT
1936                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1937
1938                // ANIMATION_TYPE_TOP_PADDING_CHANGED
1939                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1940
1941                // ANIMATION_TYPE_START_DRAG
1942                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1943
1944                // ANIMATION_TYPE_SNAP_BACK
1945                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1946
1947                // ANIMATION_TYPE_ACTIVATED_CHILD
1948                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
1949
1950                // ANIMATION_TYPE_DIMMED
1951                StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED,
1952
1953                // ANIMATION_TYPE_CHANGE_POSITION
1954                StackStateAnimator.ANIMATION_DURATION_STANDARD,
1955        };
1956
1957        static final int ANIMATION_TYPE_ADD = 0;
1958        static final int ANIMATION_TYPE_REMOVE = 1;
1959        static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2;
1960        static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3;
1961        static final int ANIMATION_TYPE_START_DRAG = 4;
1962        static final int ANIMATION_TYPE_SNAP_BACK = 5;
1963        static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6;
1964        static final int ANIMATION_TYPE_DIMMED = 7;
1965        static final int ANIMATION_TYPE_CHANGE_POSITION = 8;
1966
1967        final long eventStartTime;
1968        final View changingView;
1969        final int animationType;
1970        final AnimationFilter filter;
1971        final long length;
1972        View viewAfterChangingView;
1973
1974        AnimationEvent(View view, int type) {
1975            eventStartTime = AnimationUtils.currentAnimationTimeMillis();
1976            changingView = view;
1977            animationType = type;
1978            filter = FILTERS[type];
1979            length = LENGTHS[type];
1980        }
1981
1982        /**
1983         * Combines the length of several animation events into a single value.
1984         *
1985         * @param events The events of the lengths to combine.
1986         * @return The combined length. This is just the maximum of all length.
1987         */
1988        static long combineLength(ArrayList<AnimationEvent> events) {
1989            long length = 0;
1990            int size = events.size();
1991            for (int i = 0; i < size; i++) {
1992                length = Math.max(length, events.get(i).length);
1993            }
1994            return length;
1995        }
1996    }
1997
1998}
1999