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