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