NotificationStackScrollLayout.java revision 0dd6881ea481c855976214807c17595b34a2920a
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.stack.StackScrollState.ViewState; 43import com.android.systemui.statusbar.policy.ScrollAdapter; 44 45import java.util.ArrayList; 46 47/** 48 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack. 49 */ 50public class NotificationStackScrollLayout extends ViewGroup 51 implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter, 52 ExpandableView.OnHeightChangedListener { 53 54 private static final String TAG = "NotificationStackScrollLayout"; 55 private static final boolean DEBUG = false; 56 57 /** 58 * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}. 59 */ 60 private static final int INVALID_POINTER = -1; 61 62 private SwipeHelper mSwipeHelper; 63 private boolean mSwipingInProgress; 64 private int mCurrentStackHeight = Integer.MAX_VALUE; 65 private int mOwnScrollY; 66 private int mMaxLayoutHeight; 67 68 private VelocityTracker mVelocityTracker; 69 private OverScroller mScroller; 70 private int mTouchSlop; 71 private int mMinimumVelocity; 72 private int mMaximumVelocity; 73 private int mOverscrollDistance; 74 private int mOverflingDistance; 75 private boolean mIsBeingDragged; 76 private int mLastMotionY; 77 private int mActivePointerId; 78 79 private int mSidePaddings; 80 private Paint mDebugPaint; 81 private int mContentHeight; 82 private int mCollapsedSize; 83 private int mBottomStackPeekSize; 84 private int mEmptyMarginBottom; 85 private int mPaddingBetweenElements; 86 private int mTopPadding; 87 private boolean mListenForHeightChanges = true; 88 89 /** 90 * The algorithm which calculates the properties for our children 91 */ 92 private StackScrollAlgorithm mStackScrollAlgorithm; 93 94 /** 95 * The current State this Layout is in 96 */ 97 private StackScrollState mCurrentStackScrollState = new StackScrollState(this); 98 private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>(); 99 private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>(); 100 private ArrayList<AnimationEvent> mAnimationEvents 101 = new ArrayList<AnimationEvent>(); 102 private ArrayList<View> mSwipedOutViews = new ArrayList<View>(); 103 private final StackStateAnimator mStateAnimator = new StackStateAnimator(this); 104 105 private OnChildLocationsChangedListener mListener; 106 private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; 107 private boolean mNeedsAnimation; 108 private boolean mTopPaddingNeedsAnimation; 109 private boolean mIsExpanded = true; 110 private boolean mChildrenUpdateRequested; 111 private ViewTreeObserver.OnPreDrawListener mChildrenUpdater 112 = new ViewTreeObserver.OnPreDrawListener() { 113 @Override 114 public boolean onPreDraw() { 115 updateChildren(); 116 mChildrenUpdateRequested = false; 117 getViewTreeObserver().removeOnPreDrawListener(this); 118 return true; 119 } 120 }; 121 122 public NotificationStackScrollLayout(Context context) { 123 this(context, null); 124 } 125 126 public NotificationStackScrollLayout(Context context, AttributeSet attrs) { 127 this(context, attrs, 0); 128 } 129 130 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) { 131 this(context, attrs, defStyleAttr, 0); 132 } 133 134 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, 135 int defStyleRes) { 136 super(context, attrs, defStyleAttr, defStyleRes); 137 initView(context); 138 if (DEBUG) { 139 setWillNotDraw(false); 140 mDebugPaint = new Paint(); 141 mDebugPaint.setColor(0xffff0000); 142 mDebugPaint.setStrokeWidth(2); 143 mDebugPaint.setStyle(Paint.Style.STROKE); 144 } 145 } 146 147 @Override 148 protected void onDraw(Canvas canvas) { 149 if (DEBUG) { 150 int y = mCollapsedSize; 151 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 152 y = (int) (getLayoutHeight() - mBottomStackPeekSize - mCollapsedSize); 153 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 154 y = (int) getLayoutHeight(); 155 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 156 } 157 } 158 159 private void initView(Context context) { 160 mScroller = new OverScroller(getContext()); 161 setFocusable(true); 162 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 163 setClipChildren(false); 164 final ViewConfiguration configuration = ViewConfiguration.get(context); 165 mTouchSlop = configuration.getScaledTouchSlop(); 166 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 167 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 168 mOverscrollDistance = configuration.getScaledOverscrollDistance(); 169 mOverflingDistance = configuration.getScaledOverflingDistance(); 170 float densityScale = getResources().getDisplayMetrics().density; 171 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 172 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); 173 174 mSidePaddings = context.getResources() 175 .getDimensionPixelSize(R.dimen.notification_side_padding); 176 mCollapsedSize = context.getResources() 177 .getDimensionPixelSize(R.dimen.notification_min_height); 178 mBottomStackPeekSize = context.getResources() 179 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 180 mEmptyMarginBottom = context.getResources().getDimensionPixelSize( 181 R.dimen.notification_stack_margin_bottom); 182 mPaddingBetweenElements = context.getResources() 183 .getDimensionPixelSize(R.dimen.notification_padding); 184 mStackScrollAlgorithm = new StackScrollAlgorithm(context); 185 } 186 187 @Override 188 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 189 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 190 int mode = MeasureSpec.getMode(widthMeasureSpec); 191 int size = MeasureSpec.getSize(widthMeasureSpec); 192 int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode); 193 measureChildren(childMeasureSpec, heightMeasureSpec); 194 } 195 196 @Override 197 protected void onLayout(boolean changed, int l, int t, int r, int b) { 198 199 // we layout all our children centered on the top 200 float centerX = getWidth() / 2.0f; 201 for (int i = 0; i < getChildCount(); i++) { 202 View child = getChildAt(i); 203 float width = child.getMeasuredWidth(); 204 float height = child.getMeasuredHeight(); 205 child.layout((int) (centerX - width / 2.0f), 206 0, 207 (int) (centerX + width / 2.0f), 208 (int) height); 209 } 210 setMaxLayoutHeight(getHeight() - mEmptyMarginBottom); 211 updateContentHeight(); 212 updateScrollPositionIfNecessary(); 213 requestChildrenUpdate(); 214 } 215 216 public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) { 217 mListener = listener; 218 } 219 220 /** 221 * Returns the location the given child is currently rendered at. 222 * 223 * @param child the child to get the location for 224 * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants 225 */ 226 public int getChildLocation(View child) { 227 ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child); 228 if (childViewState == null) { 229 return ViewState.LOCATION_UNKNOWN; 230 } 231 return childViewState.location; 232 } 233 234 private void setMaxLayoutHeight(int maxLayoutHeight) { 235 mMaxLayoutHeight = maxLayoutHeight; 236 updateAlgorithmHeightAndPadding(); 237 } 238 239 private void updateAlgorithmHeightAndPadding() { 240 mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight()); 241 mStackScrollAlgorithm.setTopPadding(mTopPadding); 242 } 243 244 /** 245 * @return whether the height of the layout needs to be adapted, in order to ensure that the 246 * last child is not in the bottom stack. 247 */ 248 private boolean needsHeightAdaption() { 249 View lastChild = getLastChildNotGone(); 250 View firstChild = getFirstChildNotGone(); 251 boolean isLastChildExpanded = isViewExpanded(lastChild); 252 return isLastChildExpanded && lastChild != firstChild; 253 } 254 255 private boolean isViewExpanded(View view) { 256 if (view != null) { 257 ExpandableView expandView = (ExpandableView) view; 258 return expandView.getActualHeight() > mCollapsedSize; 259 } 260 return false; 261 } 262 263 /** 264 * Updates the children views according to the stack scroll algorithm. Call this whenever 265 * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout. 266 */ 267 private void updateChildren() { 268 mCurrentStackScrollState.setScrollY(mOwnScrollY); 269 mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState); 270 if (!isCurrentlyAnimating() && !mNeedsAnimation) { 271 applyCurrentState(); 272 } else { 273 startAnimationToState(); 274 } 275 } 276 277 private void requestChildrenUpdate() { 278 if (!mChildrenUpdateRequested) { 279 getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater); 280 mChildrenUpdateRequested = true; 281 invalidate(); 282 } 283 } 284 285 private boolean isCurrentlyAnimating() { 286 return mStateAnimator.isRunning(); 287 } 288 289 private void updateScrollPositionIfNecessary() { 290 int scrollRange = getScrollRange(); 291 if (scrollRange < mOwnScrollY) { 292 mOwnScrollY = scrollRange; 293 } 294 } 295 296 public int getTopPadding() { 297 return mTopPadding; 298 } 299 300 public void setTopPadding(int topPadding, boolean animate) { 301 if (mTopPadding != topPadding) { 302 mTopPadding = topPadding; 303 updateAlgorithmHeightAndPadding(); 304 updateContentHeight(); 305 if (animate) { 306 mTopPaddingNeedsAnimation = true; 307 mNeedsAnimation = true; 308 } 309 requestChildrenUpdate(); 310 } 311 } 312 313 /** 314 * Update the height of the stack to a new height. 315 * 316 * @param height the new height of the stack 317 */ 318 public void setStackHeight(float height) { 319 setIsExpanded(height > 0.0f); 320 int newStackHeight = (int) height; 321 int itemHeight = getItemHeight(); 322 int bottomStackPeekSize = mBottomStackPeekSize; 323 int minStackHeight = itemHeight + bottomStackPeekSize; 324 int stackHeight; 325 if (newStackHeight - mTopPadding >= minStackHeight) { 326 setTranslationY(0); 327 stackHeight = newStackHeight; 328 } else { 329 330 // We did not reach the position yet where we actually start growing, 331 // so we translate the stack upwards. 332 int translationY = (newStackHeight - minStackHeight); 333 // A slight parallax effect is introduced in order for the stack to catch up with 334 // the top card. 335 float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight; 336 partiallyThere = Math.max(0, partiallyThere); 337 translationY += (1 - partiallyThere) * bottomStackPeekSize; 338 setTranslationY(translationY - mTopPadding); 339 stackHeight = (int) (height - (translationY - mTopPadding)); 340 } 341 if (stackHeight != mCurrentStackHeight) { 342 mCurrentStackHeight = stackHeight; 343 updateAlgorithmHeightAndPadding(); 344 requestChildrenUpdate(); 345 } 346 } 347 348 /** 349 * Get the current height of the view. This is at most the msize of the view given by a the 350 * layout but it can also be made smaller by setting {@link #mCurrentStackHeight} 351 * 352 * @return either the layout height or the externally defined height, whichever is smaller 353 */ 354 private int getLayoutHeight() { 355 return Math.min(mMaxLayoutHeight, mCurrentStackHeight); 356 } 357 358 public int getItemHeight() { 359 return mCollapsedSize; 360 } 361 362 public int getBottomStackPeekSize() { 363 return mBottomStackPeekSize; 364 } 365 366 public void setLongPressListener(View.OnLongClickListener listener) { 367 mSwipeHelper.setLongPressListener(listener); 368 } 369 370 public void onChildDismissed(View v) { 371 if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); 372 final View veto = v.findViewById(R.id.veto); 373 if (veto != null && veto.getVisibility() != View.GONE) { 374 veto.performClick(); 375 } 376 setSwipingInProgress(false); 377 mSwipedOutViews.add(v); 378 } 379 380 public void onBeginDrag(View v) { 381 setSwipingInProgress(true); 382 } 383 384 public void onDragCancelled(View v) { 385 setSwipingInProgress(false); 386 } 387 388 public View getChildAtPosition(MotionEvent ev) { 389 return getChildAtPosition(ev.getX(), ev.getY()); 390 } 391 392 public ExpandableView getChildAtRawPosition(float touchX, float touchY) { 393 int[] location = new int[2]; 394 getLocationOnScreen(location); 395 return getChildAtPosition(touchX - location[0], touchY - location[1]); 396 } 397 398 public ExpandableView getChildAtPosition(float touchX, float touchY) { 399 // find the view under the pointer, accounting for GONE views 400 final int count = getChildCount(); 401 for (int childIdx = 0; childIdx < count; childIdx++) { 402 ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); 403 if (slidingChild.getVisibility() == GONE) { 404 continue; 405 } 406 float top = slidingChild.getTranslationY(); 407 float bottom = top + slidingChild.getActualHeight(); 408 int left = slidingChild.getLeft(); 409 int right = slidingChild.getRight(); 410 411 if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { 412 return slidingChild; 413 } 414 } 415 return null; 416 } 417 418 public boolean canChildBeExpanded(View v) { 419 return v instanceof ExpandableNotificationRow 420 && ((ExpandableNotificationRow) v).isExpandable(); 421 } 422 423 public void setUserExpandedChild(View v, boolean userExpanded) { 424 if (v instanceof ExpandableNotificationRow) { 425 ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); 426 } 427 } 428 429 public void setUserLockedChild(View v, boolean userLocked) { 430 if (v instanceof ExpandableNotificationRow) { 431 ((ExpandableNotificationRow) v).setUserLocked(userLocked); 432 } 433 } 434 435 public View getChildContentView(View v) { 436 return v; 437 } 438 439 public boolean canChildBeDismissed(View v) { 440 final View veto = v.findViewById(R.id.veto); 441 return (veto != null && veto.getVisibility() != View.GONE); 442 } 443 444 private void setSwipingInProgress(boolean isSwiped) { 445 mSwipingInProgress = isSwiped; 446 if(isSwiped) { 447 requestDisallowInterceptTouchEvent(true); 448 } 449 } 450 451 @Override 452 protected void onConfigurationChanged(Configuration newConfig) { 453 super.onConfigurationChanged(newConfig); 454 float densityScale = getResources().getDisplayMetrics().density; 455 mSwipeHelper.setDensityScale(densityScale); 456 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 457 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 458 initView(getContext()); 459 } 460 461 public void dismissRowAnimated(View child, int vel) { 462 mSwipeHelper.dismissChild(child, vel); 463 } 464 465 @Override 466 public boolean onTouchEvent(MotionEvent ev) { 467 boolean scrollerWantsIt = false; 468 if (!mSwipingInProgress) { 469 scrollerWantsIt = onScrollTouch(ev); 470 } 471 boolean horizontalSwipeWantsIt = false; 472 if (!mIsBeingDragged) { 473 horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); 474 } 475 return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev); 476 } 477 478 private boolean onScrollTouch(MotionEvent ev) { 479 initVelocityTrackerIfNotExists(); 480 mVelocityTracker.addMovement(ev); 481 482 final int action = ev.getAction(); 483 484 switch (action & MotionEvent.ACTION_MASK) { 485 case MotionEvent.ACTION_DOWN: { 486 if (getChildCount() == 0) { 487 return false; 488 } 489 boolean isBeingDragged = !mScroller.isFinished(); 490 setIsBeingDragged(isBeingDragged); 491 492 /* 493 * If being flinged and user touches, stop the fling. isFinished 494 * will be false if being flinged. 495 */ 496 if (!mScroller.isFinished()) { 497 mScroller.abortAnimation(); 498 } 499 500 // Remember where the motion event started 501 mLastMotionY = (int) ev.getY(); 502 mActivePointerId = ev.getPointerId(0); 503 break; 504 } 505 case MotionEvent.ACTION_MOVE: 506 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 507 if (activePointerIndex == -1) { 508 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 509 break; 510 } 511 512 final int y = (int) ev.getY(activePointerIndex); 513 int deltaY = mLastMotionY - y; 514 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 515 setIsBeingDragged(true); 516 if (deltaY > 0) { 517 deltaY -= mTouchSlop; 518 } else { 519 deltaY += mTouchSlop; 520 } 521 } 522 if (mIsBeingDragged) { 523 // Scroll to follow the motion event 524 mLastMotionY = y; 525 526 final int oldX = mScrollX; 527 final int oldY = mOwnScrollY; 528 final int range = getScrollRange(); 529 final int overscrollMode = getOverScrollMode(); 530 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || 531 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 532 533 // Calling overScrollBy will call onOverScrolled, which 534 // calls onScrollChanged if applicable. 535 if (overScrollBy(0, deltaY, 0, mOwnScrollY, 536 0, range, 0, mOverscrollDistance, true)) { 537 // Break our velocity if we hit a scroll barrier. 538 mVelocityTracker.clear(); 539 } 540 // TODO: Overscroll 541// if (canOverscroll) { 542// final int pulledToY = oldY + deltaY; 543// if (pulledToY < 0) { 544// mEdgeGlowTop.onPull((float) deltaY / getHeight()); 545// if (!mEdgeGlowBottom.isFinished()) { 546// mEdgeGlowBottom.onRelease(); 547// } 548// } else if (pulledToY > range) { 549// mEdgeGlowBottom.onPull((float) deltaY / getHeight()); 550// if (!mEdgeGlowTop.isFinished()) { 551// mEdgeGlowTop.onRelease(); 552// } 553// } 554// if (mEdgeGlowTop != null 555// && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){ 556// postInvalidateOnAnimation(); 557// } 558// } 559 } 560 break; 561 case MotionEvent.ACTION_UP: 562 if (mIsBeingDragged) { 563 final VelocityTracker velocityTracker = mVelocityTracker; 564 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 565 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 566 567 if (getChildCount() > 0) { 568 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 569 fling(-initialVelocity); 570 } else { 571 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, 572 getScrollRange())) { 573 postInvalidateOnAnimation(); 574 } 575 } 576 } 577 578 mActivePointerId = INVALID_POINTER; 579 endDrag(); 580 } 581 break; 582 case MotionEvent.ACTION_CANCEL: 583 if (mIsBeingDragged && getChildCount() > 0) { 584 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 585 postInvalidateOnAnimation(); 586 } 587 mActivePointerId = INVALID_POINTER; 588 endDrag(); 589 } 590 break; 591 case MotionEvent.ACTION_POINTER_DOWN: { 592 final int index = ev.getActionIndex(); 593 mLastMotionY = (int) ev.getY(index); 594 mActivePointerId = ev.getPointerId(index); 595 break; 596 } 597 case MotionEvent.ACTION_POINTER_UP: 598 onSecondaryPointerUp(ev); 599 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 600 break; 601 } 602 return true; 603 } 604 605 private void onSecondaryPointerUp(MotionEvent ev) { 606 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 607 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 608 final int pointerId = ev.getPointerId(pointerIndex); 609 if (pointerId == mActivePointerId) { 610 // This was our active pointer going up. Choose a new 611 // active pointer and adjust accordingly. 612 // TODO: Make this decision more intelligent. 613 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 614 mLastMotionY = (int) ev.getY(newPointerIndex); 615 mActivePointerId = ev.getPointerId(newPointerIndex); 616 if (mVelocityTracker != null) { 617 mVelocityTracker.clear(); 618 } 619 } 620 } 621 622 private void initVelocityTrackerIfNotExists() { 623 if (mVelocityTracker == null) { 624 mVelocityTracker = VelocityTracker.obtain(); 625 } 626 } 627 628 private void recycleVelocityTracker() { 629 if (mVelocityTracker != null) { 630 mVelocityTracker.recycle(); 631 mVelocityTracker = null; 632 } 633 } 634 635 private void initOrResetVelocityTracker() { 636 if (mVelocityTracker == null) { 637 mVelocityTracker = VelocityTracker.obtain(); 638 } else { 639 mVelocityTracker.clear(); 640 } 641 } 642 643 @Override 644 public void computeScroll() { 645 if (mScroller.computeScrollOffset()) { 646 // This is called at drawing time by ViewGroup. 647 int oldX = mScrollX; 648 int oldY = mOwnScrollY; 649 int x = mScroller.getCurrX(); 650 int y = mScroller.getCurrY(); 651 652 if (oldX != x || oldY != y) { 653 final int range = getScrollRange(); 654 final int overscrollMode = getOverScrollMode(); 655 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || 656 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 657 658 overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 659 0, mOverflingDistance, false); 660 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 661 662 if (canOverscroll) { 663 // TODO: Overscroll 664// if (y < 0 && oldY >= 0) { 665// mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 666// } else if (y > range && oldY <= range) { 667// mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 668// } 669 } 670 updateChildren(); 671 } 672 673 // Keep on drawing until the animation has finished. 674 postInvalidateOnAnimation(); 675 } 676 } 677 678 private void customScrollTo(int y) { 679 mOwnScrollY = y; 680 updateChildren(); 681 } 682 683 @Override 684 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 685 // Treat animating scrolls differently; see #computeScroll() for why. 686 if (!mScroller.isFinished()) { 687 final int oldX = mScrollX; 688 final int oldY = mOwnScrollY; 689 mScrollX = scrollX; 690 mOwnScrollY = scrollY; 691 invalidateParentIfNeeded(); 692 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 693 if (clampedY) { 694 mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange()); 695 } 696 updateChildren(); 697 } else { 698 customScrollTo(scrollY); 699 scrollTo(scrollX, mScrollY); 700 } 701 } 702 703 private int getScrollRange() { 704 int scrollRange = 0; 705 ExpandableView firstChild = (ExpandableView) getFirstChildNotGone(); 706 if (firstChild != null) { 707 int contentHeight = getContentHeight(); 708 int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild); 709 710 scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize); 711 if (scrollRange > 0) { 712 View lastChild = getLastChildNotGone(); 713 if (isViewExpanded(lastChild)) { 714 // last child is expanded, so we have to ensure that it can exit the 715 // bottom stack 716 scrollRange += mCollapsedSize + mPaddingBetweenElements; 717 } 718 // We want to at least be able collapse the first item and not ending in a weird 719 // end state. 720 scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize); 721 } 722 } 723 return scrollRange; 724 } 725 726 /** 727 * @return the first child which has visibility unequal to GONE 728 */ 729 private View getFirstChildNotGone() { 730 int childCount = getChildCount(); 731 for (int i = 0; i < childCount; i++) { 732 View child = getChildAt(i); 733 if (child.getVisibility() != View.GONE) { 734 return child; 735 } 736 } 737 return null; 738 } 739 740 /** 741 * @return the last child which has visibility unequal to GONE 742 */ 743 private View getLastChildNotGone() { 744 int childCount = getChildCount(); 745 for (int i = childCount - 1; i >= 0; i--) { 746 View child = getChildAt(i); 747 if (child.getVisibility() != View.GONE) { 748 return child; 749 } 750 } 751 return null; 752 } 753 754 private int getMaxExpandHeight(View view) { 755 if (view instanceof ExpandableNotificationRow) { 756 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 757 return row.getMaximumAllowedExpandHeight(); 758 } 759 return view.getHeight(); 760 } 761 762 private int getContentHeight() { 763 return mContentHeight; 764 } 765 766 private void updateContentHeight() { 767 int height = 0; 768 for (int i = 0; i < getChildCount(); i++) { 769 View child = getChildAt(i); 770 if (child.getVisibility() != View.GONE) { 771 if (height != 0) { 772 // add the padding before this element 773 height += mPaddingBetweenElements; 774 } 775 if (child instanceof ExpandableNotificationRow) { 776 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 777 height += row.getMaximumAllowedExpandHeight(); 778 } else if (child instanceof ExpandableView) { 779 ExpandableView expandableView = (ExpandableView) child; 780 height += expandableView.getActualHeight(); 781 } 782 } 783 } 784 mContentHeight = height + mTopPadding; 785 } 786 787 /** 788 * Fling the scroll view 789 * 790 * @param velocityY The initial velocity in the Y direction. Positive 791 * numbers mean that the finger/cursor is moving down the screen, 792 * which means we want to scroll towards the top. 793 */ 794 private void fling(int velocityY) { 795 if (getChildCount() > 0) { 796 int height = (int) getLayoutHeight(); 797 int bottom = getContentHeight(); 798 799 mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0, 800 Math.max(0, bottom - height), 0, height/2); 801 802 postInvalidateOnAnimation(); 803 } 804 } 805 806 private void endDrag() { 807 setIsBeingDragged(false); 808 809 recycleVelocityTracker(); 810 811 // TODO: Overscroll 812// if (mEdgeGlowTop != null) { 813// mEdgeGlowTop.onRelease(); 814// mEdgeGlowBottom.onRelease(); 815// } 816 } 817 818 @Override 819 public boolean onInterceptTouchEvent(MotionEvent ev) { 820 boolean scrollWantsIt = false; 821 if (!mSwipingInProgress) { 822 scrollWantsIt = onInterceptTouchEventScroll(ev); 823 } 824 boolean swipeWantsIt = false; 825 if (!mIsBeingDragged) { 826 swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); 827 } 828 return swipeWantsIt || scrollWantsIt || 829 super.onInterceptTouchEvent(ev); 830 } 831 832 @Override 833 protected void onViewRemoved(View child) { 834 super.onViewRemoved(child); 835 ((ExpandableView) child).setOnHeightChangedListener(null); 836 mCurrentStackScrollState.removeViewStateForView(child); 837 mStackScrollAlgorithm.notifyChildrenChanged(this); 838 updateScrollStateForRemovedChild(child); 839 if (mIsExpanded) { 840 841 if (!mChildrenToAddAnimated.contains(child)) { 842 // Generate Animations 843 mChildrenToRemoveAnimated.add(child); 844 mNeedsAnimation = true; 845 } else { 846 mChildrenToAddAnimated.remove(child); 847 } 848 } 849 } 850 851 /** 852 * Updates the scroll position when a child was removed 853 * 854 * @param removedChild the removed child 855 */ 856 private void updateScrollStateForRemovedChild(View removedChild) { 857 int startingPosition = getPositionInLinearLayout(removedChild); 858 int childHeight = removedChild.getHeight() + mPaddingBetweenElements; 859 int endPosition = startingPosition + childHeight; 860 if (endPosition <= mOwnScrollY) { 861 // This child is fully scrolled of the top, so we have to deduct its height from the 862 // scrollPosition 863 mOwnScrollY -= childHeight; 864 } else if (startingPosition < mOwnScrollY) { 865 // This child is currently being scrolled into, set the scroll position to the start of 866 // this child 867 mOwnScrollY = startingPosition; 868 } 869 } 870 871 private int getPositionInLinearLayout(View requestedChild) { 872 int position = 0; 873 for (int i = 0; i < getChildCount(); i++) { 874 View child = getChildAt(i); 875 if (child == requestedChild) { 876 return position; 877 } 878 if (child.getVisibility() != View.GONE) { 879 position += child.getHeight(); 880 if (i < getChildCount()-1) { 881 position += mPaddingBetweenElements; 882 } 883 } 884 } 885 return 0; 886 } 887 888 @Override 889 protected void onViewAdded(View child) { 890 super.onViewAdded(child); 891 mStackScrollAlgorithm.notifyChildrenChanged(this); 892 ((ExpandableView) child).setOnHeightChangedListener(this); 893 if (child.getVisibility() != View.GONE) { 894 generateAddAnimation(child); 895 } 896 } 897 898 public void generateAddAnimation(View child) { 899 if (mIsExpanded) { 900 901 // Generate Animations 902 mChildrenToAddAnimated.add(child); 903 mNeedsAnimation = true; 904 } 905 } 906 907 /** 908 * Change the position of child to a new location 909 * 910 * @param child the view to change the position for 911 * @param newIndex the new index 912 */ 913 public void changeViewPosition(View child, int newIndex) { 914 if (child != null && child.getParent() == this) { 915 // TODO: handle this 916 } 917 } 918 919 private void startAnimationToState() { 920 if (mNeedsAnimation) { 921 generateChildHierarchyEvents(); 922 mNeedsAnimation = false; 923 } 924 if (!mAnimationEvents.isEmpty()) { 925 mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState); 926 } else { 927 applyCurrentState(); 928 } 929 } 930 931 private void generateChildHierarchyEvents() { 932 generateChildAdditionEvents(); 933 generateChildRemovalEvents(); 934 generateTopPaddingEvent(); 935 mNeedsAnimation = false; 936 } 937 938 private void generateChildRemovalEvents() { 939 for (View child : mChildrenToRemoveAnimated) { 940 boolean childWasSwipedOut = mSwipedOutViews.contains(child); 941 int animationType = childWasSwipedOut 942 ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT 943 : AnimationEvent.ANIMATION_TYPE_REMOVE; 944 mAnimationEvents.add(new AnimationEvent(child, animationType)); 945 } 946 mSwipedOutViews.clear(); 947 mChildrenToRemoveAnimated.clear(); 948 } 949 950 private void generateChildAdditionEvents() { 951 for (View child : mChildrenToAddAnimated) { 952 mAnimationEvents.add(new AnimationEvent(child, 953 AnimationEvent.ANIMATION_TYPE_ADD)); 954 } 955 mChildrenToAddAnimated.clear(); 956 } 957 958 private void generateTopPaddingEvent() { 959 mAnimationEvents.add( 960 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED)); 961 mTopPaddingNeedsAnimation = false; 962 } 963 964 private boolean onInterceptTouchEventScroll(MotionEvent ev) { 965 /* 966 * This method JUST determines whether we want to intercept the motion. 967 * If we return true, onMotionEvent will be called and we do the actual 968 * scrolling there. 969 */ 970 971 /* 972 * Shortcut the most recurring case: the user is in the dragging 973 * state and he is moving his finger. We want to intercept this 974 * motion. 975 */ 976 final int action = ev.getAction(); 977 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 978 return true; 979 } 980 981 /* 982 * Don't try to intercept touch if we can't scroll anyway. 983 */ 984 if (mOwnScrollY == 0 && getScrollRange() == 0) { 985 return false; 986 } 987 988 switch (action & MotionEvent.ACTION_MASK) { 989 case MotionEvent.ACTION_MOVE: { 990 /* 991 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 992 * whether the user has moved far enough from his original down touch. 993 */ 994 995 /* 996 * Locally do absolute value. mLastMotionY is set to the y value 997 * of the down event. 998 */ 999 final int activePointerId = mActivePointerId; 1000 if (activePointerId == INVALID_POINTER) { 1001 // If we don't have a valid id, the touch down wasn't on content. 1002 break; 1003 } 1004 1005 final int pointerIndex = ev.findPointerIndex(activePointerId); 1006 if (pointerIndex == -1) { 1007 Log.e(TAG, "Invalid pointerId=" + activePointerId 1008 + " in onInterceptTouchEvent"); 1009 break; 1010 } 1011 1012 final int y = (int) ev.getY(pointerIndex); 1013 final int yDiff = Math.abs(y - mLastMotionY); 1014 if (yDiff > mTouchSlop) { 1015 setIsBeingDragged(true); 1016 mLastMotionY = y; 1017 initVelocityTrackerIfNotExists(); 1018 mVelocityTracker.addMovement(ev); 1019 } 1020 break; 1021 } 1022 1023 case MotionEvent.ACTION_DOWN: { 1024 final int y = (int) ev.getY(); 1025 if (getChildAtPosition(ev.getX(), y) == null) { 1026 setIsBeingDragged(false); 1027 recycleVelocityTracker(); 1028 break; 1029 } 1030 1031 /* 1032 * Remember location of down touch. 1033 * ACTION_DOWN always refers to pointer index 0. 1034 */ 1035 mLastMotionY = y; 1036 mActivePointerId = ev.getPointerId(0); 1037 1038 initOrResetVelocityTracker(); 1039 mVelocityTracker.addMovement(ev); 1040 /* 1041 * If being flinged and user touches the screen, initiate drag; 1042 * otherwise don't. mScroller.isFinished should be false when 1043 * being flinged. 1044 */ 1045 boolean isBeingDragged = !mScroller.isFinished(); 1046 setIsBeingDragged(isBeingDragged); 1047 break; 1048 } 1049 1050 case MotionEvent.ACTION_CANCEL: 1051 case MotionEvent.ACTION_UP: 1052 /* Release the drag */ 1053 setIsBeingDragged(false); 1054 mActivePointerId = INVALID_POINTER; 1055 recycleVelocityTracker(); 1056 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 1057 postInvalidateOnAnimation(); 1058 } 1059 break; 1060 case MotionEvent.ACTION_POINTER_UP: 1061 onSecondaryPointerUp(ev); 1062 break; 1063 } 1064 1065 /* 1066 * The only time we want to intercept motion events is if we are in the 1067 * drag mode. 1068 */ 1069 return mIsBeingDragged; 1070 } 1071 1072 private void setIsBeingDragged(boolean isDragged) { 1073 mIsBeingDragged = isDragged; 1074 if (isDragged) { 1075 requestDisallowInterceptTouchEvent(true); 1076 mSwipeHelper.removeLongPressCallback(); 1077 } 1078 } 1079 1080 @Override 1081 public void onWindowFocusChanged(boolean hasWindowFocus) { 1082 super.onWindowFocusChanged(hasWindowFocus); 1083 if (!hasWindowFocus) { 1084 mSwipeHelper.removeLongPressCallback(); 1085 } 1086 } 1087 1088 @Override 1089 public boolean isScrolledToTop() { 1090 return mOwnScrollY == 0; 1091 } 1092 1093 @Override 1094 public boolean isScrolledToBottom() { 1095 return mOwnScrollY >= getScrollRange(); 1096 } 1097 1098 @Override 1099 public View getHostView() { 1100 return this; 1101 } 1102 1103 public int getEmptyBottomMargin() { 1104 int emptyMargin = mMaxLayoutHeight - mContentHeight; 1105 if (needsHeightAdaption()) { 1106 emptyMargin = emptyMargin - mCollapsedSize - mBottomStackPeekSize; 1107 } 1108 return Math.max(emptyMargin, 0); 1109 } 1110 1111 public void onExpansionStarted() { 1112 mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); 1113 } 1114 1115 public void onExpansionStopped() { 1116 mStackScrollAlgorithm.onExpansionStopped(); 1117 } 1118 1119 private void setIsExpanded(boolean isExpanded) { 1120 mIsExpanded = isExpanded; 1121 mStackScrollAlgorithm.setIsExpanded(isExpanded); 1122 if (!isExpanded) { 1123 mOwnScrollY = 0; 1124 } 1125 } 1126 1127 @Override 1128 public void onHeightChanged(ExpandableView view) { 1129 if (mListenForHeightChanges && !isCurrentlyAnimating()) { 1130 updateContentHeight(); 1131 updateScrollPositionIfNecessary(); 1132 if (mOnHeightChangedListener != null) { 1133 mOnHeightChangedListener.onHeightChanged(view); 1134 } 1135 requestChildrenUpdate(); 1136 } 1137 } 1138 1139 public void setOnHeightChangedListener( 1140 ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { 1141 this.mOnHeightChangedListener = mOnHeightChangedListener; 1142 } 1143 1144 public void onChildAnimationFinished() { 1145 requestChildrenUpdate(); 1146 mAnimationEvents.clear(); 1147 } 1148 1149 private void applyCurrentState() { 1150 mListenForHeightChanges = false; 1151 mCurrentStackScrollState.apply(); 1152 mListenForHeightChanges = true; 1153 if (mListener != null) { 1154 mListener.onChildLocationsChanged(this); 1155 } 1156 } 1157 1158 /** 1159 * A listener that is notified when some child locations might have changed. 1160 */ 1161 public interface OnChildLocationsChangedListener { 1162 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); 1163 } 1164 1165 static class AnimationEvent { 1166 1167 static int ANIMATION_TYPE_ADD = 1; 1168 static int ANIMATION_TYPE_REMOVE = 2; 1169 static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 3; 1170 static int ANIMATION_TYPE_TOP_PADDING_CHANGED = 4; 1171 1172 final long eventStartTime; 1173 final View changingView; 1174 final int animationType; 1175 1176 AnimationEvent(View view, int type) { 1177 eventStartTime = AnimationUtils.currentAnimationTimeMillis(); 1178 changingView = view; 1179 animationType = type; 1180 } 1181 } 1182 1183} 1184