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