NotificationStackScrollLayout.java revision d7c1fae12ef0b31c225ef130e6b06445b5af53a9
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 1464 /** 1465 * Generate a remove animation for a child view. 1466 * 1467 * @param child The view to generate the remove animation for. 1468 * @return Whether an animation was generated. 1469 */ 1470 private boolean generateRemoveAnimation(View child) { 1471 if (mIsExpanded && mAnimationsEnabled) { 1472 if (!mChildrenToAddAnimated.contains(child)) { 1473 // Generate Animations 1474 mChildrenToRemoveAnimated.add(child); 1475 mNeedsAnimation = true; 1476 return true; 1477 } else { 1478 mChildrenToAddAnimated.remove(child); 1479 mFromMoreCardAdditions.remove(child); 1480 return false; 1481 } 1482 } 1483 return false; 1484 } 1485 1486 /** 1487 * Updates the scroll position when a child was removed 1488 * 1489 * @param removedChild the removed child 1490 */ 1491 private void updateScrollStateForRemovedChild(View removedChild) { 1492 int startingPosition = getPositionInLinearLayout(removedChild); 1493 int childHeight = getIntrinsicHeight(removedChild) + mPaddingBetweenElements; 1494 int endPosition = startingPosition + childHeight; 1495 if (endPosition <= mOwnScrollY) { 1496 // This child is fully scrolled of the top, so we have to deduct its height from the 1497 // scrollPosition 1498 mOwnScrollY -= childHeight; 1499 } else if (startingPosition < mOwnScrollY) { 1500 // This child is currently being scrolled into, set the scroll position to the start of 1501 // this child 1502 mOwnScrollY = startingPosition; 1503 } 1504 } 1505 1506 private int getIntrinsicHeight(View view) { 1507 if (view instanceof ExpandableView) { 1508 ExpandableView expandableView = (ExpandableView) view; 1509 return expandableView.getIntrinsicHeight(); 1510 } 1511 return view.getHeight(); 1512 } 1513 1514 private int getPositionInLinearLayout(View requestedChild) { 1515 int position = 0; 1516 for (int i = 0; i < getChildCount(); i++) { 1517 View child = getChildAt(i); 1518 if (child == requestedChild) { 1519 return position; 1520 } 1521 if (child.getVisibility() != View.GONE) { 1522 position += child.getHeight(); 1523 if (i < getChildCount()-1) { 1524 position += mPaddingBetweenElements; 1525 } 1526 } 1527 } 1528 return 0; 1529 } 1530 1531 @Override 1532 protected void onViewAdded(View child) { 1533 super.onViewAdded(child); 1534 mStackScrollAlgorithm.notifyChildrenChanged(this); 1535 ((ExpandableView) child).setOnHeightChangedListener(this); 1536 generateAddAnimation(child, false /* fromMoreCard */); 1537 } 1538 1539 public void setAnimationsEnabled(boolean animationsEnabled) { 1540 mAnimationsEnabled = animationsEnabled; 1541 } 1542 1543 public boolean isAddOrRemoveAnimationPending() { 1544 return mNeedsAnimation 1545 && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty()); 1546 } 1547 /** 1548 * Generate an animation for an added child view. 1549 * 1550 * @param child The view to be added. 1551 * @param fromMoreCard Whether this add is coming from the "more" card on lockscreen. 1552 */ 1553 public void generateAddAnimation(View child, boolean fromMoreCard) { 1554 if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) { 1555 // Generate Animations 1556 mChildrenToAddAnimated.add(child); 1557 if (fromMoreCard) { 1558 mFromMoreCardAdditions.add(child); 1559 } 1560 mNeedsAnimation = true; 1561 } 1562 } 1563 1564 /** 1565 * Change the position of child to a new location 1566 * 1567 * @param child the view to change the position for 1568 * @param newIndex the new index 1569 */ 1570 public void changeViewPosition(View child, int newIndex) { 1571 int currentIndex = indexOfChild(child); 1572 if (child != null && child.getParent() == this && currentIndex != newIndex) { 1573 mChangePositionInProgress = true; 1574 removeView(child); 1575 addView(child, newIndex); 1576 mChangePositionInProgress = false; 1577 if (mIsExpanded && mAnimationsEnabled && child.getVisibility() != View.GONE) { 1578 mChildrenChangingPositions.add(child); 1579 mNeedsAnimation = true; 1580 } 1581 } 1582 } 1583 1584 private void startAnimationToState() { 1585 if (mNeedsAnimation) { 1586 generateChildHierarchyEvents(); 1587 mNeedsAnimation = false; 1588 } 1589 if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) { 1590 mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState, 1591 mGoToFullShadeDelay); 1592 mAnimationEvents.clear(); 1593 } else { 1594 applyCurrentState(); 1595 } 1596 mGoToFullShadeDelay = 0; 1597 } 1598 1599 private void generateChildHierarchyEvents() { 1600 generateChildRemovalEvents(); 1601 generateChildAdditionEvents(); 1602 generatePositionChangeEvents(); 1603 generateSnapBackEvents(); 1604 generateDragEvents(); 1605 generateTopPaddingEvent(); 1606 generateActivateEvent(); 1607 generateDimmedEvent(); 1608 generateHideSensitiveEvent(); 1609 generateDarkEvent(); 1610 generateGoToFullShadeEvent(); 1611 generateViewResizeEvent(); 1612 mNeedsAnimation = false; 1613 } 1614 1615 private void generateViewResizeEvent() { 1616 if (mNeedViewResizeAnimation) { 1617 mAnimationEvents.add( 1618 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_VIEW_RESIZE)); 1619 } 1620 mNeedViewResizeAnimation = false; 1621 } 1622 1623 private void generateSnapBackEvents() { 1624 for (View child : mSnappedBackChildren) { 1625 mAnimationEvents.add(new AnimationEvent(child, 1626 AnimationEvent.ANIMATION_TYPE_SNAP_BACK)); 1627 } 1628 mSnappedBackChildren.clear(); 1629 } 1630 1631 private void generateDragEvents() { 1632 for (View child : mDragAnimPendingChildren) { 1633 mAnimationEvents.add(new AnimationEvent(child, 1634 AnimationEvent.ANIMATION_TYPE_START_DRAG)); 1635 } 1636 mDragAnimPendingChildren.clear(); 1637 } 1638 1639 private void generateChildRemovalEvents() { 1640 for (View child : mChildrenToRemoveAnimated) { 1641 boolean childWasSwipedOut = mSwipedOutViews.contains(child); 1642 int animationType = childWasSwipedOut 1643 ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT 1644 : AnimationEvent.ANIMATION_TYPE_REMOVE; 1645 AnimationEvent event = new AnimationEvent(child, animationType); 1646 1647 // we need to know the view after this one 1648 event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY()); 1649 mAnimationEvents.add(event); 1650 } 1651 mSwipedOutViews.clear(); 1652 mChildrenToRemoveAnimated.clear(); 1653 } 1654 1655 private void generatePositionChangeEvents() { 1656 for (View child : mChildrenChangingPositions) { 1657 mAnimationEvents.add(new AnimationEvent(child, 1658 AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)); 1659 } 1660 mChildrenChangingPositions.clear(); 1661 } 1662 1663 private void generateChildAdditionEvents() { 1664 for (View child : mChildrenToAddAnimated) { 1665 if (mFromMoreCardAdditions.contains(child)) { 1666 mAnimationEvents.add(new AnimationEvent(child, 1667 AnimationEvent.ANIMATION_TYPE_ADD, 1668 StackStateAnimator.ANIMATION_DURATION_STANDARD)); 1669 } else { 1670 mAnimationEvents.add(new AnimationEvent(child, 1671 AnimationEvent.ANIMATION_TYPE_ADD)); 1672 } 1673 } 1674 mChildrenToAddAnimated.clear(); 1675 mFromMoreCardAdditions.clear(); 1676 } 1677 1678 private void generateTopPaddingEvent() { 1679 if (mTopPaddingNeedsAnimation) { 1680 mAnimationEvents.add( 1681 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED)); 1682 } 1683 mTopPaddingNeedsAnimation = false; 1684 } 1685 1686 private void generateActivateEvent() { 1687 if (mActivateNeedsAnimation) { 1688 mAnimationEvents.add( 1689 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD)); 1690 } 1691 mActivateNeedsAnimation = false; 1692 } 1693 1694 private void generateDimmedEvent() { 1695 if (mDimmedNeedsAnimation) { 1696 mAnimationEvents.add( 1697 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED)); 1698 } 1699 mDimmedNeedsAnimation = false; 1700 } 1701 1702 private void generateHideSensitiveEvent() { 1703 if (mHideSensitiveNeedsAnimation) { 1704 mAnimationEvents.add( 1705 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_HIDE_SENSITIVE)); 1706 } 1707 mHideSensitiveNeedsAnimation = false; 1708 } 1709 1710 private void generateDarkEvent() { 1711 if (mDarkNeedsAnimation) { 1712 mAnimationEvents.add( 1713 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DARK)); 1714 } 1715 mDarkNeedsAnimation = false; 1716 } 1717 1718 private void generateGoToFullShadeEvent() { 1719 if (mGoToFullShadeNeedsAnimation) { 1720 mAnimationEvents.add( 1721 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE)); 1722 } 1723 mGoToFullShadeNeedsAnimation = false; 1724 } 1725 1726 private boolean onInterceptTouchEventScroll(MotionEvent ev) { 1727 if (!isScrollingEnabled()) { 1728 return false; 1729 } 1730 /* 1731 * This method JUST determines whether we want to intercept the motion. 1732 * If we return true, onMotionEvent will be called and we do the actual 1733 * scrolling there. 1734 */ 1735 1736 /* 1737 * Shortcut the most recurring case: the user is in the dragging 1738 * state and he is moving his finger. We want to intercept this 1739 * motion. 1740 */ 1741 final int action = ev.getAction(); 1742 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 1743 return true; 1744 } 1745 1746 switch (action & MotionEvent.ACTION_MASK) { 1747 case MotionEvent.ACTION_MOVE: { 1748 /* 1749 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1750 * whether the user has moved far enough from his original down touch. 1751 */ 1752 1753 /* 1754 * Locally do absolute value. mLastMotionY is set to the y value 1755 * of the down event. 1756 */ 1757 final int activePointerId = mActivePointerId; 1758 if (activePointerId == INVALID_POINTER) { 1759 // If we don't have a valid id, the touch down wasn't on content. 1760 break; 1761 } 1762 1763 final int pointerIndex = ev.findPointerIndex(activePointerId); 1764 if (pointerIndex == -1) { 1765 Log.e(TAG, "Invalid pointerId=" + activePointerId 1766 + " in onInterceptTouchEvent"); 1767 break; 1768 } 1769 1770 final int y = (int) ev.getY(pointerIndex); 1771 final int x = (int) ev.getX(pointerIndex); 1772 final int yDiff = Math.abs(y - mLastMotionY); 1773 final int xDiff = Math.abs(x - mDownX); 1774 if (yDiff > mTouchSlop && yDiff > xDiff) { 1775 setIsBeingDragged(true); 1776 mLastMotionY = y; 1777 mDownX = x; 1778 initVelocityTrackerIfNotExists(); 1779 mVelocityTracker.addMovement(ev); 1780 } 1781 break; 1782 } 1783 1784 case MotionEvent.ACTION_DOWN: { 1785 final int y = (int) ev.getY(); 1786 if (getChildAtPosition(ev.getX(), y) == null) { 1787 setIsBeingDragged(false); 1788 recycleVelocityTracker(); 1789 break; 1790 } 1791 1792 /* 1793 * Remember location of down touch. 1794 * ACTION_DOWN always refers to pointer index 0. 1795 */ 1796 mLastMotionY = y; 1797 mDownX = (int) ev.getX(); 1798 mActivePointerId = ev.getPointerId(0); 1799 mScrolledToTopOnFirstDown = isScrolledToTop(); 1800 1801 initOrResetVelocityTracker(); 1802 mVelocityTracker.addMovement(ev); 1803 /* 1804 * If being flinged and user touches the screen, initiate drag; 1805 * otherwise don't. mScroller.isFinished should be false when 1806 * being flinged. 1807 */ 1808 boolean isBeingDragged = !mScroller.isFinished(); 1809 setIsBeingDragged(isBeingDragged); 1810 break; 1811 } 1812 1813 case MotionEvent.ACTION_CANCEL: 1814 case MotionEvent.ACTION_UP: 1815 /* Release the drag */ 1816 setIsBeingDragged(false); 1817 mActivePointerId = INVALID_POINTER; 1818 recycleVelocityTracker(); 1819 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 1820 postInvalidateOnAnimation(); 1821 } 1822 break; 1823 case MotionEvent.ACTION_POINTER_UP: 1824 onSecondaryPointerUp(ev); 1825 break; 1826 } 1827 1828 /* 1829 * The only time we want to intercept motion events is if we are in the 1830 * drag mode. 1831 */ 1832 return mIsBeingDragged; 1833 } 1834 1835 /** 1836 * @return Whether the specified motion event is actually happening over the content. 1837 */ 1838 private boolean isInContentBounds(MotionEvent event) { 1839 return event.getY() < getHeight() - getEmptyBottomMargin(); 1840 } 1841 1842 private void setIsBeingDragged(boolean isDragged) { 1843 mIsBeingDragged = isDragged; 1844 if (isDragged) { 1845 requestDisallowInterceptTouchEvent(true); 1846 removeLongPressCallback(); 1847 } 1848 } 1849 1850 @Override 1851 public void onWindowFocusChanged(boolean hasWindowFocus) { 1852 super.onWindowFocusChanged(hasWindowFocus); 1853 if (!hasWindowFocus) { 1854 removeLongPressCallback(); 1855 } 1856 } 1857 1858 public void removeLongPressCallback() { 1859 mSwipeHelper.removeLongPressCallback(); 1860 } 1861 1862 @Override 1863 public boolean isScrolledToTop() { 1864 return mOwnScrollY == 0; 1865 } 1866 1867 @Override 1868 public boolean isScrolledToBottom() { 1869 return mOwnScrollY >= getScrollRange(); 1870 } 1871 1872 @Override 1873 public View getHostView() { 1874 return this; 1875 } 1876 1877 public int getEmptyBottomMargin() { 1878 int emptyMargin = mMaxLayoutHeight - mContentHeight - mBottomStackPeekSize; 1879 if (needsHeightAdaption()) { 1880 emptyMargin -= mBottomStackSlowDownHeight; 1881 } else { 1882 emptyMargin -= mCollapseSecondCardPadding; 1883 } 1884 return Math.max(emptyMargin, 0); 1885 } 1886 1887 public void onExpansionStarted() { 1888 mIsExpansionChanging = true; 1889 mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); 1890 } 1891 1892 public void onExpansionStopped() { 1893 mIsExpansionChanging = false; 1894 mStackScrollAlgorithm.onExpansionStopped(); 1895 if (!mIsExpanded) { 1896 mOwnScrollY = 0; 1897 } 1898 } 1899 1900 private void setIsExpanded(boolean isExpanded) { 1901 mIsExpanded = isExpanded; 1902 mStackScrollAlgorithm.setIsExpanded(isExpanded); 1903 } 1904 1905 @Override 1906 public void onHeightChanged(ExpandableView view) { 1907 updateContentHeight(); 1908 updateScrollPositionOnExpandInBottom(view); 1909 clampScrollPosition(); 1910 notifyHeightChangeListener(view); 1911 requestChildrenUpdate(); 1912 } 1913 1914 @Override 1915 public void onReset(ExpandableView view) { 1916 mRequestViewResizeAnimationOnLayout = true; 1917 } 1918 1919 private void updateScrollPositionOnExpandInBottom(ExpandableView view) { 1920 if (view instanceof ExpandableNotificationRow) { 1921 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 1922 if (row.isUserLocked()) { 1923 // We are actually expanding this view 1924 float endPosition = row.getTranslationY() + row.getActualHeight(); 1925 int stackEnd = mMaxLayoutHeight - mBottomStackPeekSize - 1926 mBottomStackSlowDownHeight; 1927 if (endPosition > stackEnd) { 1928 mOwnScrollY += endPosition - stackEnd; 1929 mDisallowScrollingInThisMotion = true; 1930 } 1931 } 1932 } 1933 } 1934 1935 public void setOnHeightChangedListener( 1936 ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { 1937 this.mOnHeightChangedListener = mOnHeightChangedListener; 1938 } 1939 1940 public void onChildAnimationFinished() { 1941 requestChildrenUpdate(); 1942 } 1943 1944 /** 1945 * See {@link AmbientState#setDimmed}. 1946 */ 1947 public void setDimmed(boolean dimmed, boolean animate) { 1948 mStackScrollAlgorithm.setDimmed(dimmed); 1949 mAmbientState.setDimmed(dimmed); 1950 updatePadding(dimmed); 1951 if (animate && mAnimationsEnabled) { 1952 mDimmedNeedsAnimation = true; 1953 mNeedsAnimation = true; 1954 } 1955 requestChildrenUpdate(); 1956 } 1957 1958 public void setHideSensitive(boolean hideSensitive, boolean animate) { 1959 if (hideSensitive != mAmbientState.isHideSensitive()) { 1960 int childCount = getChildCount(); 1961 for (int i = 0; i < childCount; i++) { 1962 ExpandableView v = (ExpandableView) getChildAt(i); 1963 v.setHideSensitiveForIntrinsicHeight(hideSensitive); 1964 } 1965 mAmbientState.setHideSensitive(hideSensitive); 1966 if (animate && mAnimationsEnabled) { 1967 mHideSensitiveNeedsAnimation = true; 1968 mNeedsAnimation = true; 1969 } 1970 requestChildrenUpdate(); 1971 } 1972 } 1973 1974 /** 1975 * See {@link AmbientState#setActivatedChild}. 1976 */ 1977 public void setActivatedChild(ActivatableNotificationView activatedChild) { 1978 mAmbientState.setActivatedChild(activatedChild); 1979 if (mAnimationsEnabled) { 1980 mActivateNeedsAnimation = true; 1981 mNeedsAnimation = true; 1982 } 1983 requestChildrenUpdate(); 1984 } 1985 1986 public ActivatableNotificationView getActivatedChild() { 1987 return mAmbientState.getActivatedChild(); 1988 } 1989 1990 private void applyCurrentState() { 1991 mCurrentStackScrollState.apply(); 1992 if (mListener != null) { 1993 mListener.onChildLocationsChanged(this); 1994 } 1995 } 1996 1997 public void setSpeedBumpView(SpeedBumpView speedBumpView) { 1998 mSpeedBumpView = speedBumpView; 1999 addView(speedBumpView); 2000 } 2001 2002 private void updateSpeedBump(boolean visible) { 2003 boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE; 2004 if (visible != notGoneBefore) { 2005 int newVisibility = visible ? VISIBLE : GONE; 2006 mSpeedBumpView.setVisibility(newVisibility); 2007 if (visible) { 2008 // Make invisible to ensure that the appear animation is played. 2009 mSpeedBumpView.setInvisible(); 2010 } else { 2011 // TODO: This doesn't really work, because the view is already set to GONE above. 2012 generateRemoveAnimation(mSpeedBumpView); 2013 } 2014 } 2015 } 2016 2017 public void goToFullShade(long delay) { 2018 updateSpeedBump(true /* visibility */); 2019 mDismissView.setInvisible(); 2020 mEmptyShadeView.setInvisible(); 2021 mGoToFullShadeNeedsAnimation = true; 2022 mGoToFullShadeDelay = delay; 2023 mNeedsAnimation = true; 2024 requestChildrenUpdate(); 2025 } 2026 2027 public void cancelExpandHelper() { 2028 mExpandHelper.cancel(); 2029 } 2030 2031 public void setIntrinsicPadding(int intrinsicPadding) { 2032 mIntrinsicPadding = intrinsicPadding; 2033 } 2034 2035 public int getIntrinsicPadding() { 2036 return mIntrinsicPadding; 2037 } 2038 2039 /** 2040 * @return the y position of the first notification 2041 */ 2042 public float getNotificationsTopY() { 2043 return mTopPadding + getTranslationY(); 2044 } 2045 2046 @Override 2047 public boolean shouldDelayChildPressedState() { 2048 return true; 2049 } 2050 2051 public void setScrimAlpha(float progress) { 2052 if (progress != mAmbientState.getScrimAmount()) { 2053 mAmbientState.setScrimAmount(progress); 2054 requestChildrenUpdate(); 2055 } 2056 } 2057 2058 /** 2059 * See {@link AmbientState#setDark}. 2060 */ 2061 public void setDark(boolean dark, boolean animate) { 2062 mAmbientState.setDark(dark); 2063 if (animate && mAnimationsEnabled) { 2064 mDarkNeedsAnimation = true; 2065 mNeedsAnimation = true; 2066 } 2067 requestChildrenUpdate(); 2068 } 2069 2070 public void setDismissView(DismissView dismissView) { 2071 mDismissView = dismissView; 2072 addView(mDismissView); 2073 } 2074 2075 public void setEmptyShadeView(EmptyShadeView emptyShadeView) { 2076 mEmptyShadeView = emptyShadeView; 2077 addView(mEmptyShadeView); 2078 } 2079 2080 public void updateEmptyShadeView(boolean visible) { 2081 int oldVisibility = mEmptyShadeView.willBeGone() ? GONE : mEmptyShadeView.getVisibility(); 2082 int newVisibility = visible ? VISIBLE : GONE; 2083 if (oldVisibility != newVisibility) { 2084 if (oldVisibility == GONE) { 2085 if (mEmptyShadeView.willBeGone()) { 2086 mEmptyShadeView.cancelAnimation(); 2087 } else { 2088 mEmptyShadeView.setInvisible(); 2089 mEmptyShadeView.setVisibility(newVisibility); 2090 } 2091 mEmptyShadeView.setWillBeGone(false); 2092 updateContentHeight(); 2093 } else { 2094 mEmptyShadeView.setWillBeGone(true); 2095 mEmptyShadeView.performVisibilityAnimation(false, new Runnable() { 2096 @Override 2097 public void run() { 2098 mEmptyShadeView.setVisibility(GONE); 2099 mEmptyShadeView.setWillBeGone(false); 2100 updateContentHeight(); 2101 } 2102 }); 2103 } 2104 } 2105 } 2106 2107 public void updateDismissView(boolean visible) { 2108 int oldVisibility = mDismissView.willBeGone() ? GONE : mDismissView.getVisibility(); 2109 int newVisibility = visible ? VISIBLE : GONE; 2110 if (oldVisibility != newVisibility) { 2111 if (oldVisibility == GONE) { 2112 if (mDismissView.willBeGone()) { 2113 mDismissView.cancelAnimation(); 2114 } else { 2115 mDismissView.setInvisible(); 2116 mDismissView.setVisibility(newVisibility); 2117 } 2118 mDismissView.setWillBeGone(false); 2119 updateContentHeight(); 2120 } else { 2121 mDismissView.setWillBeGone(true); 2122 mDismissView.performVisibilityAnimation(false, new Runnable() { 2123 @Override 2124 public void run() { 2125 mDismissView.setVisibility(GONE); 2126 mDismissView.setWillBeGone(false); 2127 updateContentHeight(); 2128 } 2129 }); 2130 } 2131 } 2132 } 2133 2134 public void setDismissAllInProgress(boolean dismissAllInProgress) { 2135 mDismissAllInProgress = dismissAllInProgress; 2136 } 2137 2138 public boolean isDismissViewNotGone() { 2139 return mDismissView.getVisibility() != View.GONE && !mDismissView.willBeGone(); 2140 } 2141 2142 public boolean isDismissViewVisible() { 2143 return mDismissView.isVisible(); 2144 } 2145 2146 public int getDismissViewHeight() { 2147 return mDismissView.getHeight() + mPaddingBetweenElementsNormal; 2148 } 2149 2150 public float getBottomMostNotificationBottom() { 2151 final int count = getChildCount(); 2152 float max = 0; 2153 for (int childIdx = 0; childIdx < count; childIdx++) { 2154 ExpandableView child = (ExpandableView) getChildAt(childIdx); 2155 if (child.getVisibility() == GONE) { 2156 continue; 2157 } 2158 float bottom = child.getTranslationY() + child.getActualHeight(); 2159 if (bottom > max) { 2160 max = bottom; 2161 } 2162 } 2163 return max + getTranslationY(); 2164 } 2165 2166 /** 2167 * @param qsMinHeight The minimum height of the quick settings including padding 2168 * See {@link StackScrollAlgorithm#updateIsSmallScreen}. 2169 */ 2170 public void updateIsSmallScreen(int qsMinHeight) { 2171 mStackScrollAlgorithm.updateIsSmallScreen(mMaxLayoutHeight - qsMinHeight); 2172 } 2173 2174 /** 2175 * A listener that is notified when some child locations might have changed. 2176 */ 2177 public interface OnChildLocationsChangedListener { 2178 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); 2179 } 2180 2181 /** 2182 * A listener that gets notified when the overscroll at the top has changed. 2183 */ 2184 public interface OnOverscrollTopChangedListener { 2185 2186 /** 2187 * Notifies a listener that the overscroll has changed. 2188 * 2189 * @param amount the amount of overscroll, in pixels 2190 * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an 2191 * unrubberbanded motion to directly expand overscroll view (e.g expand 2192 * QS) 2193 */ 2194 public void onOverscrollTopChanged(float amount, boolean isRubberbanded); 2195 2196 /** 2197 * Notify a listener that the scroller wants to escape from the scrolling motion and 2198 * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS) 2199 * 2200 * @param velocity The velocity that the Scroller had when over flinging 2201 * @param open Should the fling open or close the overscroll view. 2202 */ 2203 public void flingTopOverscroll(float velocity, boolean open); 2204 } 2205 2206 static class AnimationEvent { 2207 2208 static AnimationFilter[] FILTERS = new AnimationFilter[] { 2209 2210 // ANIMATION_TYPE_ADD 2211 new AnimationFilter() 2212 .animateAlpha() 2213 .animateHeight() 2214 .animateTopInset() 2215 .animateY() 2216 .animateZ() 2217 .hasDelays(), 2218 2219 // ANIMATION_TYPE_REMOVE 2220 new AnimationFilter() 2221 .animateAlpha() 2222 .animateHeight() 2223 .animateTopInset() 2224 .animateY() 2225 .animateZ() 2226 .hasDelays(), 2227 2228 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 2229 new AnimationFilter() 2230 .animateAlpha() 2231 .animateHeight() 2232 .animateTopInset() 2233 .animateY() 2234 .animateZ() 2235 .hasDelays(), 2236 2237 // ANIMATION_TYPE_TOP_PADDING_CHANGED 2238 new AnimationFilter() 2239 .animateAlpha() 2240 .animateHeight() 2241 .animateTopInset() 2242 .animateY() 2243 .animateDimmed() 2244 .animateScale() 2245 .animateZ(), 2246 2247 // ANIMATION_TYPE_START_DRAG 2248 new AnimationFilter() 2249 .animateAlpha(), 2250 2251 // ANIMATION_TYPE_SNAP_BACK 2252 new AnimationFilter() 2253 .animateAlpha() 2254 .animateHeight(), 2255 2256 // ANIMATION_TYPE_ACTIVATED_CHILD 2257 new AnimationFilter() 2258 .animateScale() 2259 .animateAlpha(), 2260 2261 // ANIMATION_TYPE_DIMMED 2262 new AnimationFilter() 2263 .animateY() 2264 .animateScale() 2265 .animateDimmed(), 2266 2267 // ANIMATION_TYPE_CHANGE_POSITION 2268 new AnimationFilter() 2269 .animateAlpha() 2270 .animateHeight() 2271 .animateTopInset() 2272 .animateY() 2273 .animateZ(), 2274 2275 // ANIMATION_TYPE_DARK 2276 new AnimationFilter() 2277 .animateDark(), 2278 2279 // ANIMATION_TYPE_GO_TO_FULL_SHADE 2280 new AnimationFilter() 2281 .animateAlpha() 2282 .animateHeight() 2283 .animateTopInset() 2284 .animateY() 2285 .animateDimmed() 2286 .animateScale() 2287 .animateZ() 2288 .hasDelays(), 2289 2290 // ANIMATION_TYPE_HIDE_SENSITIVE 2291 new AnimationFilter() 2292 .animateHideSensitive(), 2293 2294 // ANIMATION_TYPE_VIEW_RESIZE 2295 new AnimationFilter() 2296 .animateAlpha() 2297 .animateHeight() 2298 .animateTopInset() 2299 .animateY() 2300 .animateZ(), 2301 }; 2302 2303 static int[] LENGTHS = new int[] { 2304 2305 // ANIMATION_TYPE_ADD 2306 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 2307 2308 // ANIMATION_TYPE_REMOVE 2309 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 2310 2311 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 2312 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2313 2314 // ANIMATION_TYPE_TOP_PADDING_CHANGED 2315 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2316 2317 // ANIMATION_TYPE_START_DRAG 2318 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2319 2320 // ANIMATION_TYPE_SNAP_BACK 2321 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2322 2323 // ANIMATION_TYPE_ACTIVATED_CHILD 2324 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 2325 2326 // ANIMATION_TYPE_DIMMED 2327 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 2328 2329 // ANIMATION_TYPE_CHANGE_POSITION 2330 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2331 2332 // ANIMATION_TYPE_DARK 2333 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2334 2335 // ANIMATION_TYPE_GO_TO_FULL_SHADE 2336 StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, 2337 2338 // ANIMATION_TYPE_HIDE_SENSITIVE 2339 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2340 2341 // ANIMATION_TYPE_VIEW_RESIZE 2342 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2343 }; 2344 2345 static final int ANIMATION_TYPE_ADD = 0; 2346 static final int ANIMATION_TYPE_REMOVE = 1; 2347 static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2; 2348 static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3; 2349 static final int ANIMATION_TYPE_START_DRAG = 4; 2350 static final int ANIMATION_TYPE_SNAP_BACK = 5; 2351 static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6; 2352 static final int ANIMATION_TYPE_DIMMED = 7; 2353 static final int ANIMATION_TYPE_CHANGE_POSITION = 8; 2354 static final int ANIMATION_TYPE_DARK = 9; 2355 static final int ANIMATION_TYPE_GO_TO_FULL_SHADE = 10; 2356 static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11; 2357 static final int ANIMATION_TYPE_VIEW_RESIZE = 12; 2358 2359 final long eventStartTime; 2360 final View changingView; 2361 final int animationType; 2362 final AnimationFilter filter; 2363 final long length; 2364 View viewAfterChangingView; 2365 2366 AnimationEvent(View view, int type) { 2367 this(view, type, LENGTHS[type]); 2368 } 2369 2370 AnimationEvent(View view, int type, long length) { 2371 eventStartTime = AnimationUtils.currentAnimationTimeMillis(); 2372 changingView = view; 2373 animationType = type; 2374 filter = FILTERS[type]; 2375 this.length = length; 2376 } 2377 2378 /** 2379 * Combines the length of several animation events into a single value. 2380 * 2381 * @param events The events of the lengths to combine. 2382 * @return The combined length. Depending on the event types, this might be the maximum of 2383 * all events or the length of a specific event. 2384 */ 2385 static long combineLength(ArrayList<AnimationEvent> events) { 2386 long length = 0; 2387 int size = events.size(); 2388 for (int i = 0; i < size; i++) { 2389 AnimationEvent event = events.get(i); 2390 length = Math.max(length, event.length); 2391 if (event.animationType == ANIMATION_TYPE_GO_TO_FULL_SHADE) { 2392 return event.length; 2393 } 2394 } 2395 return length; 2396 } 2397 } 2398 2399} 2400