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