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