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