NotificationStackScrollLayout.java revision 457cc356089c61317b4c29a3e83f5fc47edb68be
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.ExpandableNotificationRow; 38import com.android.systemui.statusbar.ExpandableView; 39import com.android.systemui.statusbar.SpeedBumpView; 40import com.android.systemui.statusbar.policy.ScrollAdapter; 41import com.android.systemui.statusbar.stack.StackScrollState.ViewState; 42 43import java.util.ArrayList; 44 45/** 46 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack. 47 */ 48public class NotificationStackScrollLayout extends ViewGroup 49 implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter, 50 ExpandableView.OnHeightChangedListener { 51 52 private static final String TAG = "NotificationStackScrollLayout"; 53 private static final boolean DEBUG = false; 54 private static final float RUBBER_BAND_FACTOR = 0.35f; 55 56 /** 57 * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}. 58 */ 59 private static final int INVALID_POINTER = -1; 60 61 private SwipeHelper mSwipeHelper; 62 private boolean mSwipingInProgress; 63 private int mCurrentStackHeight = Integer.MAX_VALUE; 64 private int mOwnScrollY; 65 private int mMaxLayoutHeight; 66 67 private VelocityTracker mVelocityTracker; 68 private OverScroller mScroller; 69 private int mTouchSlop; 70 private int mMinimumVelocity; 71 private int mMaximumVelocity; 72 private int mOverflingDistance; 73 private float mMaxOverScroll; 74 private boolean mIsBeingDragged; 75 private int mLastMotionY; 76 private int mActivePointerId; 77 78 private int mSidePaddings; 79 private Paint mDebugPaint; 80 private int mContentHeight; 81 private int mCollapsedSize; 82 private int mBottomStackSlowDownHeight; 83 private int mBottomStackPeekSize; 84 private int mPaddingBetweenElements; 85 private int mPaddingBetweenElementsDimmed; 86 private int mPaddingBetweenElementsNormal; 87 private int mTopPadding; 88 89 /** 90 * The algorithm which calculates the properties for our children 91 */ 92 private StackScrollAlgorithm mStackScrollAlgorithm; 93 94 /** 95 * The current State this Layout is in 96 */ 97 private StackScrollState mCurrentStackScrollState = new StackScrollState(this); 98 private AmbientState mAmbientState = new AmbientState(); 99 private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>(); 100 private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>(); 101 private ArrayList<View> mSnappedBackChildren = new ArrayList<View>(); 102 private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>(); 103 private ArrayList<View> mChildrenChangingPositions = new ArrayList<View>(); 104 private ArrayList<AnimationEvent> mAnimationEvents 105 = new ArrayList<AnimationEvent>(); 106 private ArrayList<View> mSwipedOutViews = new ArrayList<View>(); 107 private final StackStateAnimator mStateAnimator = new StackStateAnimator(this); 108 private boolean mAnimationsEnabled; 109 110 /** 111 * The raw amount of the overScroll on the top, which is not rubber-banded. 112 */ 113 private float mOverScrolledTopPixels; 114 115 /** 116 * The raw amount of the overScroll on the bottom, which is not rubber-banded. 117 */ 118 private float mOverScrolledBottomPixels; 119 120 private OnChildLocationsChangedListener mListener; 121 private OnOverscrollTopChangedListener mOverscrollTopChangedListener; 122 private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; 123 private boolean mNeedsAnimation; 124 private boolean mTopPaddingNeedsAnimation; 125 private boolean mDimmedNeedsAnimation; 126 private boolean mActivateNeedsAnimation; 127 private boolean mIsExpanded = true; 128 private boolean mChildrenUpdateRequested; 129 private SpeedBumpView mSpeedBumpView; 130 private boolean mIsExpansionChanging; 131 private ViewTreeObserver.OnPreDrawListener mChildrenUpdater 132 = new ViewTreeObserver.OnPreDrawListener() { 133 @Override 134 public boolean onPreDraw() { 135 updateChildren(); 136 mChildrenUpdateRequested = false; 137 getViewTreeObserver().removeOnPreDrawListener(this); 138 return true; 139 } 140 }; 141 142 public NotificationStackScrollLayout(Context context) { 143 this(context, null); 144 } 145 146 public NotificationStackScrollLayout(Context context, AttributeSet attrs) { 147 this(context, attrs, 0); 148 } 149 150 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) { 151 this(context, attrs, defStyleAttr, 0); 152 } 153 154 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, 155 int defStyleRes) { 156 super(context, attrs, defStyleAttr, defStyleRes); 157 initView(context); 158 if (DEBUG) { 159 setWillNotDraw(false); 160 mDebugPaint = new Paint(); 161 mDebugPaint.setColor(0xffff0000); 162 mDebugPaint.setStrokeWidth(2); 163 mDebugPaint.setStyle(Paint.Style.STROKE); 164 } 165 } 166 167 @Override 168 protected void onDraw(Canvas canvas) { 169 if (DEBUG) { 170 int y = mCollapsedSize; 171 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 172 y = (int) (getLayoutHeight() - mBottomStackPeekSize 173 - mBottomStackSlowDownHeight); 174 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 175 y = (int) (getLayoutHeight() - mBottomStackPeekSize); 176 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 177 y = (int) getLayoutHeight(); 178 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 179 y = getHeight() - getEmptyBottomMargin(); 180 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 181 } 182 } 183 184 private void initView(Context context) { 185 mScroller = new OverScroller(getContext()); 186 setFocusable(true); 187 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 188 setClipChildren(false); 189 final ViewConfiguration configuration = ViewConfiguration.get(context); 190 mTouchSlop = configuration.getScaledTouchSlop(); 191 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 192 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 193 mOverflingDistance = configuration.getScaledOverflingDistance(); 194 float densityScale = getResources().getDisplayMetrics().density; 195 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 196 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); 197 198 mSidePaddings = context.getResources() 199 .getDimensionPixelSize(R.dimen.notification_side_padding); 200 mCollapsedSize = context.getResources() 201 .getDimensionPixelSize(R.dimen.notification_min_height); 202 mBottomStackPeekSize = context.getResources() 203 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 204 mStackScrollAlgorithm = new StackScrollAlgorithm(context); 205 mPaddingBetweenElementsDimmed = context.getResources() 206 .getDimensionPixelSize(R.dimen.notification_padding_dimmed); 207 mPaddingBetweenElementsNormal = context.getResources() 208 .getDimensionPixelSize(R.dimen.notification_padding); 209 updatePadding(false); 210 } 211 212 private void updatePadding(boolean dimmed) { 213 mPaddingBetweenElements = dimmed 214 ? mPaddingBetweenElementsDimmed 215 : mPaddingBetweenElementsNormal; 216 mBottomStackSlowDownHeight = mStackScrollAlgorithm.getBottomStackSlowDownLength(); 217 updateContentHeight(); 218 } 219 220 @Override 221 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 222 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 223 int mode = MeasureSpec.getMode(widthMeasureSpec); 224 int size = MeasureSpec.getSize(widthMeasureSpec); 225 int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode); 226 measureChildren(childMeasureSpec, heightMeasureSpec); 227 } 228 229 @Override 230 protected void onLayout(boolean changed, int l, int t, int r, int b) { 231 232 // we layout all our children centered on the top 233 float centerX = getWidth() / 2.0f; 234 for (int i = 0; i < getChildCount(); i++) { 235 View child = getChildAt(i); 236 float width = child.getMeasuredWidth(); 237 float height = child.getMeasuredHeight(); 238 child.layout((int) (centerX - width / 2.0f), 239 0, 240 (int) (centerX + width / 2.0f), 241 (int) height); 242 } 243 setMaxLayoutHeight(getHeight()); 244 updateContentHeight(); 245 updateScrollPositionIfNecessary(); 246 requestChildrenUpdate(); 247 } 248 249 public void updateSpeedBumpIndex(int newIndex) { 250 int currentIndex = indexOfChild(mSpeedBumpView); 251 252 // If we are currently layouted before the new speed bump index, we have to decrease it. 253 boolean validIndex = newIndex > 0; 254 if (newIndex > getChildCount() - 1) { 255 validIndex = false; 256 newIndex = -1; 257 } 258 if (validIndex && currentIndex != newIndex) { 259 changeViewPosition(mSpeedBumpView, newIndex); 260 } 261 updateSpeedBump(validIndex); 262 mAmbientState.setSpeedBumpIndex(newIndex); 263 } 264 265 public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) { 266 mListener = listener; 267 } 268 269 /** 270 * Returns the location the given child is currently rendered at. 271 * 272 * @param child the child to get the location for 273 * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants 274 */ 275 public int getChildLocation(View child) { 276 ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child); 277 if (childViewState == null) { 278 return ViewState.LOCATION_UNKNOWN; 279 } 280 return childViewState.location; 281 } 282 283 private void setMaxLayoutHeight(int maxLayoutHeight) { 284 mMaxLayoutHeight = maxLayoutHeight; 285 updateAlgorithmHeightAndPadding(); 286 } 287 288 private void updateAlgorithmHeightAndPadding() { 289 mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight()); 290 mStackScrollAlgorithm.setTopPadding(mTopPadding); 291 } 292 293 /** 294 * @return whether the height of the layout needs to be adapted, in order to ensure that the 295 * last child is not in the bottom stack. 296 */ 297 private boolean needsHeightAdaption() { 298 return getNotGoneChildCount() > 1; 299 } 300 301 private boolean isViewExpanded(View view) { 302 if (view != null) { 303 ExpandableView expandView = (ExpandableView) view; 304 return expandView.getActualHeight() > mCollapsedSize; 305 } 306 return false; 307 } 308 309 /** 310 * Updates the children views according to the stack scroll algorithm. Call this whenever 311 * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout. 312 */ 313 private void updateChildren() { 314 mAmbientState.setScrollY(mOwnScrollY); 315 mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState); 316 if (!isCurrentlyAnimating() && !mNeedsAnimation) { 317 applyCurrentState(); 318 } else { 319 startAnimationToState(); 320 } 321 } 322 323 private void requestChildrenUpdate() { 324 if (!mChildrenUpdateRequested) { 325 getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater); 326 mChildrenUpdateRequested = true; 327 invalidate(); 328 } 329 } 330 331 private boolean isCurrentlyAnimating() { 332 return mStateAnimator.isRunning(); 333 } 334 335 private void updateScrollPositionIfNecessary() { 336 int scrollRange = getScrollRange(); 337 if (scrollRange < mOwnScrollY) { 338 mOwnScrollY = scrollRange; 339 } 340 } 341 342 public int getTopPadding() { 343 return mTopPadding; 344 } 345 346 public void setTopPadding(int topPadding, boolean animate) { 347 if (mTopPadding != topPadding) { 348 mTopPadding = topPadding; 349 updateAlgorithmHeightAndPadding(); 350 updateContentHeight(); 351 if (animate && mAnimationsEnabled && mIsExpanded) { 352 mTopPaddingNeedsAnimation = true; 353 mNeedsAnimation = true; 354 } 355 requestChildrenUpdate(); 356 if (mOnHeightChangedListener != null) { 357 mOnHeightChangedListener.onHeightChanged(null); 358 } 359 } 360 } 361 362 /** 363 * Update the height of the stack to a new height. 364 * 365 * @param height the new height of the stack 366 */ 367 public void setStackHeight(float height) { 368 setIsExpanded(height > 0.0f); 369 int newStackHeight = (int) height; 370 int itemHeight = getItemHeight(); 371 int bottomStackPeekSize = mBottomStackPeekSize; 372 int minStackHeight = itemHeight + bottomStackPeekSize; 373 int stackHeight; 374 if (newStackHeight - mTopPadding >= minStackHeight) { 375 setTranslationY(0); 376 stackHeight = newStackHeight; 377 } else { 378 379 // We did not reach the position yet where we actually start growing, 380 // so we translate the stack upwards. 381 int translationY = (newStackHeight - minStackHeight); 382 // A slight parallax effect is introduced in order for the stack to catch up with 383 // the top card. 384 float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight; 385 partiallyThere = Math.max(0, partiallyThere); 386 translationY += (1 - partiallyThere) * bottomStackPeekSize; 387 setTranslationY(translationY - mTopPadding); 388 stackHeight = (int) (height - (translationY - mTopPadding)); 389 } 390 if (stackHeight != mCurrentStackHeight) { 391 mCurrentStackHeight = stackHeight; 392 updateAlgorithmHeightAndPadding(); 393 requestChildrenUpdate(); 394 } 395 } 396 397 /** 398 * Get the current height of the view. This is at most the msize of the view given by a the 399 * layout but it can also be made smaller by setting {@link #mCurrentStackHeight} 400 * 401 * @return either the layout height or the externally defined height, whichever is smaller 402 */ 403 private int getLayoutHeight() { 404 return Math.min(mMaxLayoutHeight, mCurrentStackHeight); 405 } 406 407 public int getItemHeight() { 408 return mCollapsedSize; 409 } 410 411 public int getBottomStackPeekSize() { 412 return mBottomStackPeekSize; 413 } 414 415 public void setLongPressListener(View.OnLongClickListener listener) { 416 mSwipeHelper.setLongPressListener(listener); 417 } 418 419 public void onChildDismissed(View v) { 420 if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); 421 final View veto = v.findViewById(R.id.veto); 422 if (veto != null && veto.getVisibility() != View.GONE) { 423 veto.performClick(); 424 } 425 setSwipingInProgress(false); 426 if (mDragAnimPendingChildren.contains(v)) { 427 // We start the swipe and finish it in the same frame, we don't want any animation 428 // for the drag 429 mDragAnimPendingChildren.remove(v); 430 } 431 mSwipedOutViews.add(v); 432 mAmbientState.onDragFinished(v); 433 } 434 435 @Override 436 public void onChildSnappedBack(View animView) { 437 mAmbientState.onDragFinished(animView); 438 if (!mDragAnimPendingChildren.contains(animView)) { 439 if (mAnimationsEnabled) { 440 mSnappedBackChildren.add(animView); 441 mNeedsAnimation = true; 442 } 443 requestChildrenUpdate(); 444 } else { 445 // We start the swipe and snap back in the same frame, we don't want any animation 446 mDragAnimPendingChildren.remove(animView); 447 } 448 } 449 450 @Override 451 public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { 452 return false; 453 } 454 455 public void onBeginDrag(View v) { 456 setSwipingInProgress(true); 457 mAmbientState.onBeginDrag(v); 458 if (mAnimationsEnabled) { 459 mDragAnimPendingChildren.add(v); 460 mNeedsAnimation = true; 461 } 462 requestChildrenUpdate(); 463 } 464 465 public void onDragCancelled(View v) { 466 setSwipingInProgress(false); 467 } 468 469 public View getChildAtPosition(MotionEvent ev) { 470 return getChildAtPosition(ev.getX(), ev.getY()); 471 } 472 473 public ExpandableView getChildAtRawPosition(float touchX, float touchY) { 474 int[] location = new int[2]; 475 getLocationOnScreen(location); 476 return getChildAtPosition(touchX - location[0], touchY - location[1]); 477 } 478 479 public ExpandableView getChildAtPosition(float touchX, float touchY) { 480 // find the view under the pointer, accounting for GONE views 481 final int count = getChildCount(); 482 for (int childIdx = 0; childIdx < count; childIdx++) { 483 ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); 484 if (slidingChild.getVisibility() == GONE) { 485 continue; 486 } 487 float top = slidingChild.getTranslationY(); 488 float bottom = top + slidingChild.getActualHeight(); 489 int left = slidingChild.getLeft(); 490 int right = slidingChild.getRight(); 491 492 if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { 493 return slidingChild; 494 } 495 } 496 return null; 497 } 498 499 public boolean canChildBeExpanded(View v) { 500 return v instanceof ExpandableNotificationRow 501 && ((ExpandableNotificationRow) v).isExpandable(); 502 } 503 504 public void setUserExpandedChild(View v, boolean userExpanded) { 505 if (v instanceof ExpandableNotificationRow) { 506 ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); 507 } 508 } 509 510 public void setUserLockedChild(View v, boolean userLocked) { 511 if (v instanceof ExpandableNotificationRow) { 512 ((ExpandableNotificationRow) v).setUserLocked(userLocked); 513 } 514 } 515 516 public View getChildContentView(View v) { 517 return v; 518 } 519 520 public boolean canChildBeDismissed(View v) { 521 final View veto = v.findViewById(R.id.veto); 522 return (veto != null && veto.getVisibility() != View.GONE); 523 } 524 525 private void setSwipingInProgress(boolean isSwiped) { 526 mSwipingInProgress = isSwiped; 527 if(isSwiped) { 528 requestDisallowInterceptTouchEvent(true); 529 } 530 } 531 532 @Override 533 protected void onConfigurationChanged(Configuration newConfig) { 534 super.onConfigurationChanged(newConfig); 535 float densityScale = getResources().getDisplayMetrics().density; 536 mSwipeHelper.setDensityScale(densityScale); 537 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 538 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 539 initView(getContext()); 540 } 541 542 public void dismissRowAnimated(View child, int vel) { 543 mSwipeHelper.dismissChild(child, vel); 544 } 545 546 @Override 547 public boolean onTouchEvent(MotionEvent ev) { 548 if (!isEnabled()) { 549 return false; 550 } 551 boolean scrollerWantsIt = false; 552 if (!mSwipingInProgress) { 553 scrollerWantsIt = onScrollTouch(ev); 554 } 555 boolean horizontalSwipeWantsIt = false; 556 if (!mIsBeingDragged) { 557 horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); 558 } 559 return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev); 560 } 561 562 private boolean onScrollTouch(MotionEvent ev) { 563 initVelocityTrackerIfNotExists(); 564 mVelocityTracker.addMovement(ev); 565 566 final int action = ev.getAction(); 567 568 switch (action & MotionEvent.ACTION_MASK) { 569 case MotionEvent.ACTION_DOWN: { 570 if (getChildCount() == 0 || !isInContentBounds(ev)) { 571 return false; 572 } 573 boolean isBeingDragged = !mScroller.isFinished(); 574 setIsBeingDragged(isBeingDragged); 575 576 /* 577 * If being flinged and user touches, stop the fling. isFinished 578 * will be false if being flinged. 579 */ 580 if (!mScroller.isFinished()) { 581 mScroller.forceFinished(true); 582 } 583 584 // Remember where the motion event started 585 mLastMotionY = (int) ev.getY(); 586 mActivePointerId = ev.getPointerId(0); 587 break; 588 } 589 case MotionEvent.ACTION_MOVE: 590 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 591 if (activePointerIndex == -1) { 592 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 593 break; 594 } 595 596 final int y = (int) ev.getY(activePointerIndex); 597 int deltaY = mLastMotionY - y; 598 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 599 setIsBeingDragged(true); 600 if (deltaY > 0) { 601 deltaY -= mTouchSlop; 602 } else { 603 deltaY += mTouchSlop; 604 } 605 } 606 if (mIsBeingDragged) { 607 // Scroll to follow the motion event 608 mLastMotionY = y; 609 final int range = getScrollRange(); 610 611 float scrollAmount; 612 if (deltaY < 0) { 613 scrollAmount = overScrollDown(deltaY); 614 } else { 615 scrollAmount = overScrollUp(deltaY, range); 616 } 617 618 // Calling overScrollBy will call onOverScrolled, which 619 // calls onScrollChanged if applicable. 620 if (scrollAmount != 0.0f) { 621 // The scrolling motion could not be compensated with the 622 // existing overScroll, we have to scroll the view 623 overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY, 624 0, range, 0, getHeight() / 2, true); 625 } 626 } 627 break; 628 case MotionEvent.ACTION_UP: 629 if (mIsBeingDragged) { 630 final VelocityTracker velocityTracker = mVelocityTracker; 631 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 632 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 633 634 if (getChildCount() > 0) { 635 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 636 fling(-initialVelocity); 637 } else { 638 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, 639 getScrollRange())) { 640 postInvalidateOnAnimation(); 641 } 642 } 643 } 644 645 mActivePointerId = INVALID_POINTER; 646 endDrag(); 647 } 648 break; 649 case MotionEvent.ACTION_CANCEL: 650 if (mIsBeingDragged && getChildCount() > 0) { 651 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 652 postInvalidateOnAnimation(); 653 } 654 mActivePointerId = INVALID_POINTER; 655 endDrag(); 656 } 657 break; 658 case MotionEvent.ACTION_POINTER_DOWN: { 659 final int index = ev.getActionIndex(); 660 mLastMotionY = (int) ev.getY(index); 661 mActivePointerId = ev.getPointerId(index); 662 break; 663 } 664 case MotionEvent.ACTION_POINTER_UP: 665 onSecondaryPointerUp(ev); 666 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 667 break; 668 } 669 return true; 670 } 671 672 /** 673 * Perform a scroll upwards and adapt the overscroll amounts accordingly 674 * 675 * @param deltaY The amount to scroll upwards, has to be positive. 676 * @return The amount of scrolling to be performed by the scroller, 677 * not handled by the overScroll amount. 678 */ 679 private float overScrollUp(int deltaY, int range) { 680 deltaY = Math.max(deltaY, 0); 681 float currentTopAmount = getCurrentOverScrollAmount(true); 682 float newTopAmount = currentTopAmount - deltaY; 683 if (currentTopAmount > 0) { 684 setOverScrollAmount(newTopAmount, true /* onTop */, 685 false /* animate */); 686 } 687 // Top overScroll might not grab all scrolling motion, 688 // we have to scroll as well. 689 float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f; 690 float newScrollY = mOwnScrollY + scrollAmount; 691 if (newScrollY > range) { 692 float currentBottomPixels = getCurrentOverScrolledPixels(false); 693 // We overScroll on the top 694 setOverScrolledPixels(currentBottomPixels + newScrollY - range, 695 false /* onTop */, 696 false /* animate */); 697 mOwnScrollY = range; 698 scrollAmount = 0.0f; 699 } 700 return scrollAmount; 701 } 702 703 /** 704 * Perform a scroll downward and adapt the overscroll amounts accordingly 705 * 706 * @param deltaY The amount to scroll downwards, has to be negative. 707 * @return The amount of scrolling to be performed by the scroller, 708 * not handled by the overScroll amount. 709 */ 710 private float overScrollDown(int deltaY) { 711 deltaY = Math.min(deltaY, 0); 712 float currentBottomAmount = getCurrentOverScrollAmount(false); 713 float newBottomAmount = currentBottomAmount + deltaY; 714 if (currentBottomAmount > 0) { 715 setOverScrollAmount(newBottomAmount, false /* onTop */, 716 false /* animate */); 717 } 718 // Bottom overScroll might not grab all scrolling motion, 719 // we have to scroll as well. 720 float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f; 721 float newScrollY = mOwnScrollY + scrollAmount; 722 if (newScrollY < 0) { 723 float currentTopPixels = getCurrentOverScrolledPixels(true); 724 // We overScroll on the top 725 setOverScrolledPixels(currentTopPixels - newScrollY, 726 true /* onTop */, 727 false /* animate */); 728 mOwnScrollY = 0; 729 scrollAmount = 0.0f; 730 } 731 return scrollAmount; 732 } 733 734 private void onSecondaryPointerUp(MotionEvent ev) { 735 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 736 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 737 final int pointerId = ev.getPointerId(pointerIndex); 738 if (pointerId == mActivePointerId) { 739 // This was our active pointer going up. Choose a new 740 // active pointer and adjust accordingly. 741 // TODO: Make this decision more intelligent. 742 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 743 mLastMotionY = (int) ev.getY(newPointerIndex); 744 mActivePointerId = ev.getPointerId(newPointerIndex); 745 if (mVelocityTracker != null) { 746 mVelocityTracker.clear(); 747 } 748 } 749 } 750 751 private void initVelocityTrackerIfNotExists() { 752 if (mVelocityTracker == null) { 753 mVelocityTracker = VelocityTracker.obtain(); 754 } 755 } 756 757 private void recycleVelocityTracker() { 758 if (mVelocityTracker != null) { 759 mVelocityTracker.recycle(); 760 mVelocityTracker = null; 761 } 762 } 763 764 private void initOrResetVelocityTracker() { 765 if (mVelocityTracker == null) { 766 mVelocityTracker = VelocityTracker.obtain(); 767 } else { 768 mVelocityTracker.clear(); 769 } 770 } 771 772 @Override 773 public void computeScroll() { 774 if (mScroller.computeScrollOffset()) { 775 // This is called at drawing time by ViewGroup. 776 int oldX = mScrollX; 777 int oldY = mOwnScrollY; 778 int x = mScroller.getCurrX(); 779 int y = mScroller.getCurrY(); 780 781 if (oldX != x || oldY != y) { 782 final int range = getScrollRange(); 783 if (y < 0 && oldY >= 0 || y > range && oldY <= range) { 784 float currVelocity = mScroller.getCurrVelocity(); 785 if (currVelocity >= mMinimumVelocity) { 786 mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance; 787 } 788 } 789 790 overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 791 0, (int) (mMaxOverScroll), false); 792 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 793 } 794 795 // Keep on drawing until the animation has finished. 796 postInvalidateOnAnimation(); 797 } 798 } 799 800 @Override 801 protected boolean overScrollBy(int deltaX, int deltaY, 802 int scrollX, int scrollY, 803 int scrollRangeX, int scrollRangeY, 804 int maxOverScrollX, int maxOverScrollY, 805 boolean isTouchEvent) { 806 807 int newScrollY = scrollY + deltaY; 808 809 final int top = -maxOverScrollY; 810 final int bottom = maxOverScrollY + scrollRangeY; 811 812 boolean clampedY = false; 813 if (newScrollY > bottom) { 814 newScrollY = bottom; 815 clampedY = true; 816 } else if (newScrollY < top) { 817 newScrollY = top; 818 clampedY = true; 819 } 820 821 onOverScrolled(0, newScrollY, false, clampedY); 822 823 return clampedY; 824 } 825 826 /** 827 * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded 828 * overscroll effect based on numPixels. By default this will also cancel animations on the 829 * same overScroll edge. 830 * 831 * @param numPixels The amount of pixels to overScroll by. These will be scaled according to 832 * the rubber-banding logic. 833 * @param onTop Should the effect be applied on top of the scroller. 834 * @param animate Should an animation be performed. 835 */ 836 public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) { 837 setOverScrollAmount(numPixels * RUBBER_BAND_FACTOR, onTop, animate, true); 838 } 839 840 /** 841 * Set the effective overScroll amount which will be directly reflected in the layout. 842 * By default this will also cancel animations on the same overScroll edge. 843 * 844 * @param amount The amount to overScroll by. 845 * @param onTop Should the effect be applied on top of the scroller. 846 * @param animate Should an animation be performed. 847 */ 848 public void setOverScrollAmount(float amount, boolean onTop, boolean animate) { 849 setOverScrollAmount(amount, onTop, animate, true); 850 } 851 852 /** 853 * Set the effective overScroll amount which will be directly reflected in the layout. 854 * 855 * @param amount The amount to overScroll by. 856 * @param onTop Should the effect be applied on top of the scroller. 857 * @param animate Should an animation be performed. 858 * @param cancelAnimators Should running animations be cancelled. 859 */ 860 public void setOverScrollAmount(float amount, boolean onTop, boolean animate, 861 boolean cancelAnimators) { 862 if (cancelAnimators) { 863 mStateAnimator.cancelOverScrollAnimators(onTop); 864 } 865 setOverScrollAmountInternal(amount, onTop, animate); 866 } 867 868 private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate) { 869 amount = Math.max(0, amount); 870 if (animate) { 871 mStateAnimator.animateOverScrollToAmount(amount, onTop); 872 } else { 873 setOverScrolledPixels(amount / RUBBER_BAND_FACTOR, onTop); 874 mAmbientState.setOverScrollAmount(amount, onTop); 875 requestChildrenUpdate(); 876 if (onTop) { 877 float scrollAmount = mOwnScrollY < 0 ? -mOwnScrollY : 0; 878 notifyOverscrollTopListener(scrollAmount + amount); 879 } 880 } 881 } 882 883 private void notifyOverscrollTopListener(float amount) { 884 if (mOverscrollTopChangedListener != null) { 885 mOverscrollTopChangedListener.onOverscrollTopChanged(amount); 886 } 887 } 888 889 public void setOverscrollTopChangedListener( 890 OnOverscrollTopChangedListener overscrollTopChangedListener) { 891 mOverscrollTopChangedListener = overscrollTopChangedListener; 892 } 893 894 public float getCurrentOverScrollAmount(boolean top) { 895 return mAmbientState.getOverScrollAmount(top); 896 } 897 898 public float getCurrentOverScrolledPixels(boolean top) { 899 return top? mOverScrolledTopPixels : mOverScrolledBottomPixels; 900 } 901 902 private void setOverScrolledPixels(float amount, boolean onTop) { 903 if (onTop) { 904 mOverScrolledTopPixels = amount; 905 } else { 906 mOverScrolledBottomPixels = amount; 907 } 908 } 909 910 private void customScrollTo(int y) { 911 mOwnScrollY = y; 912 updateChildren(); 913 } 914 915 @Override 916 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 917 // Treat animating scrolls differently; see #computeScroll() for why. 918 if (!mScroller.isFinished()) { 919 final int oldX = mScrollX; 920 final int oldY = mOwnScrollY; 921 mScrollX = scrollX; 922 mOwnScrollY = scrollY; 923 if (clampedY) { 924 springBack(); 925 } else { 926 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 927 invalidateParentIfNeeded(); 928 updateChildren(); 929 float overScrollTop = getCurrentOverScrollAmount(true); 930 if (mOwnScrollY < 0) { 931 notifyOverscrollTopListener(-mOwnScrollY + overScrollTop); 932 } else { 933 notifyOverscrollTopListener(overScrollTop); 934 } 935 } 936 } else { 937 customScrollTo(scrollY); 938 scrollTo(scrollX, mScrollY); 939 } 940 } 941 942 private void springBack() { 943 int scrollRange = getScrollRange(); 944 boolean overScrolledTop = mOwnScrollY <= 0; 945 boolean overScrolledBottom = mOwnScrollY >= scrollRange; 946 if (overScrolledTop || overScrolledBottom) { 947 boolean onTop; 948 float newAmount; 949 if (overScrolledTop) { 950 onTop = true; 951 newAmount = -mOwnScrollY; 952 mOwnScrollY = 0; 953 } else { 954 onTop = false; 955 newAmount = mOwnScrollY - scrollRange; 956 mOwnScrollY = scrollRange; 957 } 958 setOverScrollAmount(newAmount, onTop, false); 959 setOverScrollAmount(0.0f, onTop, true); 960 mScroller.forceFinished(true); 961 } 962 } 963 964 private int getScrollRange() { 965 int scrollRange = 0; 966 ExpandableView firstChild = (ExpandableView) getFirstChildNotGone(); 967 if (firstChild != null) { 968 int contentHeight = getContentHeight(); 969 int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild); 970 scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize 971 + mBottomStackSlowDownHeight); 972 if (scrollRange > 0) { 973 View lastChild = getLastChildNotGone(); 974 // We want to at least be able collapse the first item and not ending in a weird 975 // end state. 976 scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize); 977 } 978 } 979 return scrollRange; 980 } 981 982 /** 983 * @return the first child which has visibility unequal to GONE 984 */ 985 private View getFirstChildNotGone() { 986 int childCount = getChildCount(); 987 for (int i = 0; i < childCount; i++) { 988 View child = getChildAt(i); 989 if (child.getVisibility() != View.GONE) { 990 return child; 991 } 992 } 993 return null; 994 } 995 996 /** 997 * @return The first child which has visibility unequal to GONE which is currently below the 998 * given translationY or equal to it. 999 */ 1000 private View getFirstChildBelowTranlsationY(float translationY) { 1001 int childCount = getChildCount(); 1002 for (int i = 0; i < childCount; i++) { 1003 View child = getChildAt(i); 1004 if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) { 1005 return child; 1006 } 1007 } 1008 return null; 1009 } 1010 1011 /** 1012 * @return the last child which has visibility unequal to GONE 1013 */ 1014 public View getLastChildNotGone() { 1015 int childCount = getChildCount(); 1016 for (int i = childCount - 1; i >= 0; i--) { 1017 View child = getChildAt(i); 1018 if (child.getVisibility() != View.GONE) { 1019 return child; 1020 } 1021 } 1022 return null; 1023 } 1024 1025 /** 1026 * @return the number of children which have visibility unequal to GONE 1027 */ 1028 public int getNotGoneChildCount() { 1029 int childCount = getChildCount(); 1030 int count = 0; 1031 for (int i = 0; i < childCount; i++) { 1032 View child = getChildAt(i); 1033 if (child.getVisibility() != View.GONE) { 1034 count++; 1035 } 1036 } 1037 return count; 1038 } 1039 1040 private int getMaxExpandHeight(View view) { 1041 if (view instanceof ExpandableNotificationRow) { 1042 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 1043 return row.getIntrinsicHeight(); 1044 } 1045 return view.getHeight(); 1046 } 1047 1048 private int getContentHeight() { 1049 return mContentHeight; 1050 } 1051 1052 private void updateContentHeight() { 1053 int height = 0; 1054 for (int i = 0; i < getChildCount(); i++) { 1055 View child = getChildAt(i); 1056 if (child.getVisibility() != View.GONE) { 1057 if (height != 0) { 1058 // add the padding before this element 1059 height += mPaddingBetweenElements; 1060 } 1061 if (child instanceof ExpandableNotificationRow) { 1062 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 1063 height += row.getIntrinsicHeight(); 1064 } else if (child instanceof ExpandableView) { 1065 ExpandableView expandableView = (ExpandableView) child; 1066 height += expandableView.getActualHeight(); 1067 } 1068 } 1069 } 1070 mContentHeight = height + mTopPadding; 1071 } 1072 1073 /** 1074 * Fling the scroll view 1075 * 1076 * @param velocityY The initial velocity in the Y direction. Positive 1077 * numbers mean that the finger/cursor is moving down the screen, 1078 * which means we want to scroll towards the top. 1079 */ 1080 private void fling(int velocityY) { 1081 if (getChildCount() > 0) { 1082 int scrollRange = getScrollRange(); 1083 1084 float topAmount = getCurrentOverScrollAmount(true); 1085 float bottomAmount = getCurrentOverScrollAmount(false); 1086 if (velocityY < 0 && topAmount > 0) { 1087 mOwnScrollY -= (int) topAmount; 1088 setOverScrollAmount(0, true, false); 1089 mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR 1090 * mOverflingDistance + topAmount; 1091 } else if (velocityY > 0 && bottomAmount > 0) { 1092 mOwnScrollY += bottomAmount; 1093 setOverScrollAmount(0, false, false); 1094 mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR 1095 * mOverflingDistance + bottomAmount; 1096 } else { 1097 // it will be set once we reach the boundary 1098 mMaxOverScroll = 0.0f; 1099 } 1100 mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0, 1101 Math.max(0, scrollRange), 0, Integer.MAX_VALUE / 2); 1102 1103 postInvalidateOnAnimation(); 1104 } 1105 } 1106 1107 private void endDrag() { 1108 setIsBeingDragged(false); 1109 1110 recycleVelocityTracker(); 1111 1112 if (getCurrentOverScrollAmount(true /* onTop */) > 0) { 1113 setOverScrollAmount(0, true /* onTop */, true /* animate */); 1114 } 1115 if (getCurrentOverScrollAmount(false /* onTop */) > 0) { 1116 setOverScrollAmount(0, false /* onTop */, true /* animate */); 1117 } 1118 } 1119 1120 @Override 1121 public boolean onInterceptTouchEvent(MotionEvent ev) { 1122 boolean scrollWantsIt = false; 1123 if (!mSwipingInProgress) { 1124 scrollWantsIt = onInterceptTouchEventScroll(ev); 1125 } 1126 boolean swipeWantsIt = false; 1127 if (!mIsBeingDragged) { 1128 swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); 1129 } 1130 return swipeWantsIt || scrollWantsIt || 1131 super.onInterceptTouchEvent(ev); 1132 } 1133 1134 @Override 1135 protected void onViewRemoved(View child) { 1136 super.onViewRemoved(child); 1137 mStackScrollAlgorithm.notifyChildrenChanged(this); 1138 if (mChildrenChangingPositions.contains(child)) { 1139 // This is only a position change, don't do anything special 1140 return; 1141 } 1142 ((ExpandableView) child).setOnHeightChangedListener(null); 1143 mCurrentStackScrollState.removeViewStateForView(child); 1144 updateScrollStateForRemovedChild(child); 1145 boolean animationGenerated = generateRemoveAnimation(child); 1146 if (animationGenerated && !mSwipedOutViews.contains(child)) { 1147 // Add this view to an overlay in order to ensure that it will still be temporary 1148 // drawn when removed 1149 getOverlay().add(child); 1150 } 1151 } 1152 1153 /** 1154 * Generate a remove animation for a child view. 1155 * 1156 * @param child The view to generate the remove animation for. 1157 * @return Whether an animation was generated. 1158 */ 1159 private boolean generateRemoveAnimation(View child) { 1160 if (mIsExpanded && mAnimationsEnabled) { 1161 if (!mChildrenToAddAnimated.contains(child)) { 1162 // Generate Animations 1163 mChildrenToRemoveAnimated.add(child); 1164 mNeedsAnimation = true; 1165 return true; 1166 } else { 1167 mChildrenToAddAnimated.remove(child); 1168 return false; 1169 } 1170 } 1171 return false; 1172 } 1173 1174 /** 1175 * Updates the scroll position when a child was removed 1176 * 1177 * @param removedChild the removed child 1178 */ 1179 private void updateScrollStateForRemovedChild(View removedChild) { 1180 int startingPosition = getPositionInLinearLayout(removedChild); 1181 int childHeight = removedChild.getHeight() + mPaddingBetweenElements; 1182 int endPosition = startingPosition + childHeight; 1183 if (endPosition <= mOwnScrollY) { 1184 // This child is fully scrolled of the top, so we have to deduct its height from the 1185 // scrollPosition 1186 mOwnScrollY -= childHeight; 1187 } else if (startingPosition < mOwnScrollY) { 1188 // This child is currently being scrolled into, set the scroll position to the start of 1189 // this child 1190 mOwnScrollY = startingPosition; 1191 } 1192 } 1193 1194 private int getPositionInLinearLayout(View requestedChild) { 1195 int position = 0; 1196 for (int i = 0; i < getChildCount(); i++) { 1197 View child = getChildAt(i); 1198 if (child == requestedChild) { 1199 return position; 1200 } 1201 if (child.getVisibility() != View.GONE) { 1202 position += child.getHeight(); 1203 if (i < getChildCount()-1) { 1204 position += mPaddingBetweenElements; 1205 } 1206 } 1207 } 1208 return 0; 1209 } 1210 1211 @Override 1212 protected void onViewAdded(View child) { 1213 super.onViewAdded(child); 1214 mStackScrollAlgorithm.notifyChildrenChanged(this); 1215 ((ExpandableView) child).setOnHeightChangedListener(this); 1216 generateAddAnimation(child); 1217 } 1218 1219 public void setAnimationsEnabled(boolean animationsEnabled) { 1220 mAnimationsEnabled = animationsEnabled; 1221 } 1222 1223 public boolean isAddOrRemoveAnimationPending() { 1224 return mNeedsAnimation 1225 && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty()); 1226 } 1227 /** 1228 * Generate an animation for an added child view. 1229 * 1230 * @param child The view to be added. 1231 */ 1232 public void generateAddAnimation(View child) { 1233 if (mIsExpanded && mAnimationsEnabled && !mChildrenChangingPositions.contains(child)) { 1234 // Generate Animations 1235 mChildrenToAddAnimated.add(child); 1236 mNeedsAnimation = true; 1237 } 1238 } 1239 1240 /** 1241 * Change the position of child to a new location 1242 * 1243 * @param child the view to change the position for 1244 * @param newIndex the new index 1245 */ 1246 public void changeViewPosition(View child, int newIndex) { 1247 if (child != null && child.getParent() == this) { 1248 mChildrenChangingPositions.add(child); 1249 removeView(child); 1250 addView(child, newIndex); 1251 mNeedsAnimation = true; 1252 } 1253 } 1254 1255 private void startAnimationToState() { 1256 if (mNeedsAnimation) { 1257 generateChildHierarchyEvents(); 1258 mNeedsAnimation = false; 1259 } 1260 if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) { 1261 mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState); 1262 mAnimationEvents.clear(); 1263 } else { 1264 applyCurrentState(); 1265 } 1266 } 1267 1268 private void generateChildHierarchyEvents() { 1269 generateChildRemovalEvents(); 1270 generateChildAdditionEvents(); 1271 generatePositionChangeEvents(); 1272 generateSnapBackEvents(); 1273 generateDragEvents(); 1274 generateTopPaddingEvent(); 1275 generateActivateEvent(); 1276 generateDimmedEvent(); 1277 mNeedsAnimation = false; 1278 } 1279 1280 private void generateSnapBackEvents() { 1281 for (View child : mSnappedBackChildren) { 1282 mAnimationEvents.add(new AnimationEvent(child, 1283 AnimationEvent.ANIMATION_TYPE_SNAP_BACK)); 1284 } 1285 mSnappedBackChildren.clear(); 1286 } 1287 1288 private void generateDragEvents() { 1289 for (View child : mDragAnimPendingChildren) { 1290 mAnimationEvents.add(new AnimationEvent(child, 1291 AnimationEvent.ANIMATION_TYPE_START_DRAG)); 1292 } 1293 mDragAnimPendingChildren.clear(); 1294 } 1295 1296 private void generateChildRemovalEvents() { 1297 for (View child : mChildrenToRemoveAnimated) { 1298 boolean childWasSwipedOut = mSwipedOutViews.contains(child); 1299 int animationType = childWasSwipedOut 1300 ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT 1301 : AnimationEvent.ANIMATION_TYPE_REMOVE; 1302 AnimationEvent event = new AnimationEvent(child, animationType); 1303 1304 // we need to know the view after this one 1305 event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY()); 1306 mAnimationEvents.add(event); 1307 } 1308 mSwipedOutViews.clear(); 1309 mChildrenToRemoveAnimated.clear(); 1310 } 1311 1312 private void generatePositionChangeEvents() { 1313 for (View child : mChildrenChangingPositions) { 1314 mAnimationEvents.add(new AnimationEvent(child, 1315 AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)); 1316 } 1317 mChildrenChangingPositions.clear(); 1318 } 1319 1320 private void generateChildAdditionEvents() { 1321 for (View child : mChildrenToAddAnimated) { 1322 mAnimationEvents.add(new AnimationEvent(child, 1323 AnimationEvent.ANIMATION_TYPE_ADD)); 1324 } 1325 mChildrenToAddAnimated.clear(); 1326 } 1327 1328 private void generateTopPaddingEvent() { 1329 if (mTopPaddingNeedsAnimation) { 1330 mAnimationEvents.add( 1331 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED)); 1332 } 1333 mTopPaddingNeedsAnimation = false; 1334 } 1335 1336 private void generateActivateEvent() { 1337 if (mActivateNeedsAnimation) { 1338 mAnimationEvents.add( 1339 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD)); 1340 } 1341 mActivateNeedsAnimation = false; 1342 } 1343 1344 private void generateDimmedEvent() { 1345 if (mDimmedNeedsAnimation) { 1346 mAnimationEvents.add( 1347 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED)); 1348 } 1349 mDimmedNeedsAnimation = false; 1350 } 1351 1352 private boolean onInterceptTouchEventScroll(MotionEvent ev) { 1353 /* 1354 * This method JUST determines whether we want to intercept the motion. 1355 * If we return true, onMotionEvent will be called and we do the actual 1356 * scrolling there. 1357 */ 1358 1359 /* 1360 * Shortcut the most recurring case: the user is in the dragging 1361 * state and he is moving his finger. We want to intercept this 1362 * motion. 1363 */ 1364 final int action = ev.getAction(); 1365 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 1366 return true; 1367 } 1368 1369 /* 1370 * Don't try to intercept touch if we can't scroll anyway. 1371 */ 1372 if (mOwnScrollY == 0 && getScrollRange() == 0) { 1373 return false; 1374 } 1375 1376 switch (action & MotionEvent.ACTION_MASK) { 1377 case MotionEvent.ACTION_MOVE: { 1378 /* 1379 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1380 * whether the user has moved far enough from his original down touch. 1381 */ 1382 1383 /* 1384 * Locally do absolute value. mLastMotionY is set to the y value 1385 * of the down event. 1386 */ 1387 final int activePointerId = mActivePointerId; 1388 if (activePointerId == INVALID_POINTER) { 1389 // If we don't have a valid id, the touch down wasn't on content. 1390 break; 1391 } 1392 1393 final int pointerIndex = ev.findPointerIndex(activePointerId); 1394 if (pointerIndex == -1) { 1395 Log.e(TAG, "Invalid pointerId=" + activePointerId 1396 + " in onInterceptTouchEvent"); 1397 break; 1398 } 1399 1400 final int y = (int) ev.getY(pointerIndex); 1401 final int yDiff = Math.abs(y - mLastMotionY); 1402 if (yDiff > mTouchSlop) { 1403 setIsBeingDragged(true); 1404 mLastMotionY = y; 1405 initVelocityTrackerIfNotExists(); 1406 mVelocityTracker.addMovement(ev); 1407 } 1408 break; 1409 } 1410 1411 case MotionEvent.ACTION_DOWN: { 1412 final int y = (int) ev.getY(); 1413 if (getChildAtPosition(ev.getX(), y) == null) { 1414 setIsBeingDragged(false); 1415 recycleVelocityTracker(); 1416 break; 1417 } 1418 1419 /* 1420 * Remember location of down touch. 1421 * ACTION_DOWN always refers to pointer index 0. 1422 */ 1423 mLastMotionY = y; 1424 mActivePointerId = ev.getPointerId(0); 1425 1426 initOrResetVelocityTracker(); 1427 mVelocityTracker.addMovement(ev); 1428 /* 1429 * If being flinged and user touches the screen, initiate drag; 1430 * otherwise don't. mScroller.isFinished should be false when 1431 * being flinged. 1432 */ 1433 boolean isBeingDragged = !mScroller.isFinished(); 1434 setIsBeingDragged(isBeingDragged); 1435 break; 1436 } 1437 1438 case MotionEvent.ACTION_CANCEL: 1439 case MotionEvent.ACTION_UP: 1440 /* Release the drag */ 1441 setIsBeingDragged(false); 1442 mActivePointerId = INVALID_POINTER; 1443 recycleVelocityTracker(); 1444 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 1445 postInvalidateOnAnimation(); 1446 } 1447 break; 1448 case MotionEvent.ACTION_POINTER_UP: 1449 onSecondaryPointerUp(ev); 1450 break; 1451 } 1452 1453 /* 1454 * The only time we want to intercept motion events is if we are in the 1455 * drag mode. 1456 */ 1457 return mIsBeingDragged; 1458 } 1459 1460 /** 1461 * @return Whether the specified motion event is actually happening over the content. 1462 */ 1463 private boolean isInContentBounds(MotionEvent event) { 1464 return event.getY() < getHeight() - getEmptyBottomMargin(); 1465 } 1466 1467 private void setIsBeingDragged(boolean isDragged) { 1468 mIsBeingDragged = isDragged; 1469 if (isDragged) { 1470 requestDisallowInterceptTouchEvent(true); 1471 mSwipeHelper.removeLongPressCallback(); 1472 } 1473 } 1474 1475 @Override 1476 public void onWindowFocusChanged(boolean hasWindowFocus) { 1477 super.onWindowFocusChanged(hasWindowFocus); 1478 if (!hasWindowFocus) { 1479 mSwipeHelper.removeLongPressCallback(); 1480 } 1481 } 1482 1483 @Override 1484 public boolean isScrolledToTop() { 1485 return mOwnScrollY == 0; 1486 } 1487 1488 @Override 1489 public boolean isScrolledToBottom() { 1490 return mOwnScrollY >= getScrollRange(); 1491 } 1492 1493 @Override 1494 public View getHostView() { 1495 return this; 1496 } 1497 1498 public int getEmptyBottomMargin() { 1499 int emptyMargin = mMaxLayoutHeight - mContentHeight; 1500 if (needsHeightAdaption()) { 1501 emptyMargin = emptyMargin - mBottomStackSlowDownHeight - mBottomStackPeekSize; 1502 } else { 1503 emptyMargin = emptyMargin - mBottomStackPeekSize; 1504 } 1505 return Math.max(emptyMargin, 0); 1506 } 1507 1508 public void onExpansionStarted() { 1509 mIsExpansionChanging = true; 1510 mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); 1511 } 1512 1513 public void onExpansionStopped() { 1514 mIsExpansionChanging = false; 1515 mStackScrollAlgorithm.onExpansionStopped(); 1516 } 1517 1518 private void setIsExpanded(boolean isExpanded) { 1519 mIsExpanded = isExpanded; 1520 mStackScrollAlgorithm.setIsExpanded(isExpanded); 1521 if (!isExpanded) { 1522 mOwnScrollY = 0; 1523 mSpeedBumpView.collapse(); 1524 } 1525 } 1526 1527 @Override 1528 public void onHeightChanged(ExpandableView view) { 1529 updateContentHeight(); 1530 updateScrollPositionIfNecessary(); 1531 if (mOnHeightChangedListener != null) { 1532 mOnHeightChangedListener.onHeightChanged(view); 1533 } 1534 requestChildrenUpdate(); 1535 } 1536 1537 public void setOnHeightChangedListener( 1538 ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { 1539 this.mOnHeightChangedListener = mOnHeightChangedListener; 1540 } 1541 1542 public void onChildAnimationFinished() { 1543 requestChildrenUpdate(); 1544 } 1545 1546 /** 1547 * See {@link AmbientState#setDimmed}. 1548 */ 1549 public void setDimmed(boolean dimmed, boolean animate) { 1550 mStackScrollAlgorithm.setDimmed(dimmed); 1551 mAmbientState.setDimmed(dimmed); 1552 updatePadding(dimmed); 1553 if (animate && mAnimationsEnabled) { 1554 mDimmedNeedsAnimation = true; 1555 mNeedsAnimation = true; 1556 } 1557 requestChildrenUpdate(); 1558 } 1559 1560 /** 1561 * See {@link AmbientState#setActivatedChild}. 1562 */ 1563 public void setActivatedChild(View activatedChild) { 1564 mAmbientState.setActivatedChild(activatedChild); 1565 if (mAnimationsEnabled) { 1566 mActivateNeedsAnimation = true; 1567 mNeedsAnimation = true; 1568 } 1569 requestChildrenUpdate(); 1570 } 1571 1572 public View getActivatedChild() { 1573 return mAmbientState.getActivatedChild(); 1574 } 1575 1576 private void applyCurrentState() { 1577 mCurrentStackScrollState.apply(); 1578 if (mListener != null) { 1579 mListener.onChildLocationsChanged(this); 1580 } 1581 } 1582 1583 public void setSpeedBumpView(SpeedBumpView speedBumpView) { 1584 mSpeedBumpView = speedBumpView; 1585 addView(speedBumpView); 1586 } 1587 1588 private void updateSpeedBump(boolean visible) { 1589 boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE; 1590 if (visible != notGoneBefore) { 1591 int newVisibility = visible ? VISIBLE : GONE; 1592 mSpeedBumpView.setVisibility(newVisibility); 1593 if (visible) { 1594 mSpeedBumpView.collapse(); 1595 // Make invisible to ensure that the appear animation is played. 1596 mSpeedBumpView.setInvisible(); 1597 if (!mIsExpansionChanging) { 1598 generateAddAnimation(mSpeedBumpView); 1599 } 1600 } else { 1601 mSpeedBumpView.performVisibilityAnimation(false); 1602 generateRemoveAnimation(mSpeedBumpView); 1603 } 1604 } 1605 } 1606 1607 public void goToFullShade() { 1608 updateSpeedBump(true); 1609 } 1610 1611 /** 1612 * @return the y position of the first notification 1613 */ 1614 public float getNotificationsTopY() { 1615 return mTopPadding + getTranslationY(); 1616 } 1617 1618 /** 1619 * A listener that is notified when some child locations might have changed. 1620 */ 1621 public interface OnChildLocationsChangedListener { 1622 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); 1623 } 1624 1625 /** 1626 * A listener that gets notified when the overscroll at the top has changed. 1627 */ 1628 public interface OnOverscrollTopChangedListener { 1629 public void onOverscrollTopChanged(float amount); 1630 } 1631 1632 static class AnimationEvent { 1633 1634 static AnimationFilter[] FILTERS = new AnimationFilter[] { 1635 1636 // ANIMATION_TYPE_ADD 1637 new AnimationFilter() 1638 .animateAlpha() 1639 .animateHeight() 1640 .animateTopInset() 1641 .animateY() 1642 .animateZ() 1643 .hasDelays(), 1644 1645 // ANIMATION_TYPE_REMOVE 1646 new AnimationFilter() 1647 .animateAlpha() 1648 .animateHeight() 1649 .animateTopInset() 1650 .animateY() 1651 .animateZ() 1652 .hasDelays(), 1653 1654 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 1655 new AnimationFilter() 1656 .animateAlpha() 1657 .animateHeight() 1658 .animateTopInset() 1659 .animateY() 1660 .animateZ() 1661 .hasDelays(), 1662 1663 // ANIMATION_TYPE_TOP_PADDING_CHANGED 1664 new AnimationFilter() 1665 .animateAlpha() 1666 .animateHeight() 1667 .animateTopInset() 1668 .animateY() 1669 .animateDimmed() 1670 .animateScale() 1671 .animateZ(), 1672 1673 // ANIMATION_TYPE_START_DRAG 1674 new AnimationFilter() 1675 .animateAlpha(), 1676 1677 // ANIMATION_TYPE_SNAP_BACK 1678 new AnimationFilter() 1679 .animateAlpha(), 1680 1681 // ANIMATION_TYPE_ACTIVATED_CHILD 1682 new AnimationFilter() 1683 .animateScale() 1684 .animateAlpha(), 1685 1686 // ANIMATION_TYPE_DIMMED 1687 new AnimationFilter() 1688 .animateY() 1689 .animateScale() 1690 .animateDimmed(), 1691 1692 // ANIMATION_TYPE_CHANGE_POSITION 1693 new AnimationFilter() 1694 .animateAlpha() 1695 .animateHeight() 1696 .animateTopInset() 1697 .animateY() 1698 .animateZ() 1699 }; 1700 1701 static int[] LENGTHS = new int[] { 1702 1703 // ANIMATION_TYPE_ADD 1704 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 1705 1706 // ANIMATION_TYPE_REMOVE 1707 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 1708 1709 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 1710 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1711 1712 // ANIMATION_TYPE_TOP_PADDING_CHANGED 1713 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1714 1715 // ANIMATION_TYPE_START_DRAG 1716 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1717 1718 // ANIMATION_TYPE_SNAP_BACK 1719 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1720 1721 // ANIMATION_TYPE_ACTIVATED_CHILD 1722 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 1723 1724 // ANIMATION_TYPE_DIMMED 1725 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 1726 1727 // ANIMATION_TYPE_CHANGE_POSITION 1728 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1729 }; 1730 1731 static final int ANIMATION_TYPE_ADD = 0; 1732 static final int ANIMATION_TYPE_REMOVE = 1; 1733 static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2; 1734 static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3; 1735 static final int ANIMATION_TYPE_START_DRAG = 4; 1736 static final int ANIMATION_TYPE_SNAP_BACK = 5; 1737 static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6; 1738 static final int ANIMATION_TYPE_DIMMED = 7; 1739 static final int ANIMATION_TYPE_CHANGE_POSITION = 8; 1740 1741 final long eventStartTime; 1742 final View changingView; 1743 final int animationType; 1744 final AnimationFilter filter; 1745 final long length; 1746 View viewAfterChangingView; 1747 1748 AnimationEvent(View view, int type) { 1749 eventStartTime = AnimationUtils.currentAnimationTimeMillis(); 1750 changingView = view; 1751 animationType = type; 1752 filter = FILTERS[type]; 1753 length = LENGTHS[type]; 1754 } 1755 1756 /** 1757 * Combines the length of several animation events into a single value. 1758 * 1759 * @param events The events of the lengths to combine. 1760 * @return The combined length. This is just the maximum of all length. 1761 */ 1762 static long combineLength(ArrayList<AnimationEvent> events) { 1763 long length = 0; 1764 int size = events.size(); 1765 for (int i = 0; i < size; i++) { 1766 length = Math.max(length, events.get(i).length); 1767 } 1768 return length; 1769 } 1770 } 1771 1772} 1773