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