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