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