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