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