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