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