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