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