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