NotificationStackScrollLayout.java revision 98fb09c2b2dbf57803a8737ee7b73cf167721312
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 if (mOnHeightChangedListener != null) { 311 mOnHeightChangedListener.onHeightChanged(null); 312 } 313 } 314 } 315 316 /** 317 * Update the height of the stack to a new height. 318 * 319 * @param height the new height of the stack 320 */ 321 public void setStackHeight(float height) { 322 setIsExpanded(height > 0.0f); 323 int newStackHeight = (int) height; 324 int itemHeight = getItemHeight(); 325 int bottomStackPeekSize = mBottomStackPeekSize; 326 int minStackHeight = itemHeight + bottomStackPeekSize; 327 int stackHeight; 328 if (newStackHeight - mTopPadding >= minStackHeight) { 329 setTranslationY(0); 330 stackHeight = newStackHeight; 331 } else { 332 333 // We did not reach the position yet where we actually start growing, 334 // so we translate the stack upwards. 335 int translationY = (newStackHeight - minStackHeight); 336 // A slight parallax effect is introduced in order for the stack to catch up with 337 // the top card. 338 float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight; 339 partiallyThere = Math.max(0, partiallyThere); 340 translationY += (1 - partiallyThere) * bottomStackPeekSize; 341 setTranslationY(translationY - mTopPadding); 342 stackHeight = (int) (height - (translationY - mTopPadding)); 343 } 344 if (stackHeight != mCurrentStackHeight) { 345 mCurrentStackHeight = stackHeight; 346 updateAlgorithmHeightAndPadding(); 347 requestChildrenUpdate(); 348 } 349 } 350 351 /** 352 * Get the current height of the view. This is at most the msize of the view given by a the 353 * layout but it can also be made smaller by setting {@link #mCurrentStackHeight} 354 * 355 * @return either the layout height or the externally defined height, whichever is smaller 356 */ 357 private int getLayoutHeight() { 358 return Math.min(mMaxLayoutHeight, mCurrentStackHeight); 359 } 360 361 public int getItemHeight() { 362 return mCollapsedSize; 363 } 364 365 public int getBottomStackPeekSize() { 366 return mBottomStackPeekSize; 367 } 368 369 public void setLongPressListener(View.OnLongClickListener listener) { 370 mSwipeHelper.setLongPressListener(listener); 371 } 372 373 public void onChildDismissed(View v) { 374 if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); 375 final View veto = v.findViewById(R.id.veto); 376 if (veto != null && veto.getVisibility() != View.GONE) { 377 veto.performClick(); 378 } 379 setSwipingInProgress(false); 380 mSwipedOutViews.add(v); 381 } 382 383 public void onBeginDrag(View v) { 384 setSwipingInProgress(true); 385 } 386 387 public void onDragCancelled(View v) { 388 setSwipingInProgress(false); 389 } 390 391 public View getChildAtPosition(MotionEvent ev) { 392 return getChildAtPosition(ev.getX(), ev.getY()); 393 } 394 395 public ExpandableView getChildAtRawPosition(float touchX, float touchY) { 396 int[] location = new int[2]; 397 getLocationOnScreen(location); 398 return getChildAtPosition(touchX - location[0], touchY - location[1]); 399 } 400 401 public ExpandableView getChildAtPosition(float touchX, float touchY) { 402 // find the view under the pointer, accounting for GONE views 403 final int count = getChildCount(); 404 for (int childIdx = 0; childIdx < count; childIdx++) { 405 ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); 406 if (slidingChild.getVisibility() == GONE) { 407 continue; 408 } 409 float top = slidingChild.getTranslationY(); 410 float bottom = top + slidingChild.getActualHeight(); 411 int left = slidingChild.getLeft(); 412 int right = slidingChild.getRight(); 413 414 if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { 415 return slidingChild; 416 } 417 } 418 return null; 419 } 420 421 public boolean canChildBeExpanded(View v) { 422 return v instanceof ExpandableNotificationRow 423 && ((ExpandableNotificationRow) v).isExpandable(); 424 } 425 426 public void setUserExpandedChild(View v, boolean userExpanded) { 427 if (v instanceof ExpandableNotificationRow) { 428 ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); 429 } 430 } 431 432 public void setUserLockedChild(View v, boolean userLocked) { 433 if (v instanceof ExpandableNotificationRow) { 434 ((ExpandableNotificationRow) v).setUserLocked(userLocked); 435 } 436 } 437 438 public View getChildContentView(View v) { 439 return v; 440 } 441 442 public boolean canChildBeDismissed(View v) { 443 final View veto = v.findViewById(R.id.veto); 444 return (veto != null && veto.getVisibility() != View.GONE); 445 } 446 447 private void setSwipingInProgress(boolean isSwiped) { 448 mSwipingInProgress = isSwiped; 449 if(isSwiped) { 450 requestDisallowInterceptTouchEvent(true); 451 } 452 } 453 454 @Override 455 protected void onConfigurationChanged(Configuration newConfig) { 456 super.onConfigurationChanged(newConfig); 457 float densityScale = getResources().getDisplayMetrics().density; 458 mSwipeHelper.setDensityScale(densityScale); 459 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 460 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 461 initView(getContext()); 462 } 463 464 public void dismissRowAnimated(View child, int vel) { 465 mSwipeHelper.dismissChild(child, vel); 466 } 467 468 @Override 469 public boolean onTouchEvent(MotionEvent ev) { 470 boolean scrollerWantsIt = false; 471 if (!mSwipingInProgress) { 472 scrollerWantsIt = onScrollTouch(ev); 473 } 474 boolean horizontalSwipeWantsIt = false; 475 if (!mIsBeingDragged) { 476 horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); 477 } 478 return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev); 479 } 480 481 private boolean onScrollTouch(MotionEvent ev) { 482 initVelocityTrackerIfNotExists(); 483 mVelocityTracker.addMovement(ev); 484 485 final int action = ev.getAction(); 486 487 switch (action & MotionEvent.ACTION_MASK) { 488 case MotionEvent.ACTION_DOWN: { 489 if (getChildCount() == 0) { 490 return false; 491 } 492 boolean isBeingDragged = !mScroller.isFinished(); 493 setIsBeingDragged(isBeingDragged); 494 495 /* 496 * If being flinged and user touches, stop the fling. isFinished 497 * will be false if being flinged. 498 */ 499 if (!mScroller.isFinished()) { 500 mScroller.abortAnimation(); 501 } 502 503 // Remember where the motion event started 504 mLastMotionY = (int) ev.getY(); 505 mActivePointerId = ev.getPointerId(0); 506 break; 507 } 508 case MotionEvent.ACTION_MOVE: 509 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 510 if (activePointerIndex == -1) { 511 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 512 break; 513 } 514 515 final int y = (int) ev.getY(activePointerIndex); 516 int deltaY = mLastMotionY - y; 517 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 518 setIsBeingDragged(true); 519 if (deltaY > 0) { 520 deltaY -= mTouchSlop; 521 } else { 522 deltaY += mTouchSlop; 523 } 524 } 525 if (mIsBeingDragged) { 526 // Scroll to follow the motion event 527 mLastMotionY = y; 528 529 final int oldX = mScrollX; 530 final int oldY = mOwnScrollY; 531 final int range = getScrollRange(); 532 final int overscrollMode = getOverScrollMode(); 533 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || 534 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 535 536 // Calling overScrollBy will call onOverScrolled, which 537 // calls onScrollChanged if applicable. 538 if (overScrollBy(0, deltaY, 0, mOwnScrollY, 539 0, range, 0, mOverscrollDistance, true)) { 540 // Break our velocity if we hit a scroll barrier. 541 mVelocityTracker.clear(); 542 } 543 // TODO: Overscroll 544// if (canOverscroll) { 545// final int pulledToY = oldY + deltaY; 546// if (pulledToY < 0) { 547// mEdgeGlowTop.onPull((float) deltaY / getHeight()); 548// if (!mEdgeGlowBottom.isFinished()) { 549// mEdgeGlowBottom.onRelease(); 550// } 551// } else if (pulledToY > range) { 552// mEdgeGlowBottom.onPull((float) deltaY / getHeight()); 553// if (!mEdgeGlowTop.isFinished()) { 554// mEdgeGlowTop.onRelease(); 555// } 556// } 557// if (mEdgeGlowTop != null 558// && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){ 559// postInvalidateOnAnimation(); 560// } 561// } 562 } 563 break; 564 case MotionEvent.ACTION_UP: 565 if (mIsBeingDragged) { 566 final VelocityTracker velocityTracker = mVelocityTracker; 567 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 568 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 569 570 if (getChildCount() > 0) { 571 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 572 fling(-initialVelocity); 573 } else { 574 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, 575 getScrollRange())) { 576 postInvalidateOnAnimation(); 577 } 578 } 579 } 580 581 mActivePointerId = INVALID_POINTER; 582 endDrag(); 583 } 584 break; 585 case MotionEvent.ACTION_CANCEL: 586 if (mIsBeingDragged && getChildCount() > 0) { 587 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 588 postInvalidateOnAnimation(); 589 } 590 mActivePointerId = INVALID_POINTER; 591 endDrag(); 592 } 593 break; 594 case MotionEvent.ACTION_POINTER_DOWN: { 595 final int index = ev.getActionIndex(); 596 mLastMotionY = (int) ev.getY(index); 597 mActivePointerId = ev.getPointerId(index); 598 break; 599 } 600 case MotionEvent.ACTION_POINTER_UP: 601 onSecondaryPointerUp(ev); 602 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 603 break; 604 } 605 return true; 606 } 607 608 private void onSecondaryPointerUp(MotionEvent ev) { 609 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 610 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 611 final int pointerId = ev.getPointerId(pointerIndex); 612 if (pointerId == mActivePointerId) { 613 // This was our active pointer going up. Choose a new 614 // active pointer and adjust accordingly. 615 // TODO: Make this decision more intelligent. 616 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 617 mLastMotionY = (int) ev.getY(newPointerIndex); 618 mActivePointerId = ev.getPointerId(newPointerIndex); 619 if (mVelocityTracker != null) { 620 mVelocityTracker.clear(); 621 } 622 } 623 } 624 625 private void initVelocityTrackerIfNotExists() { 626 if (mVelocityTracker == null) { 627 mVelocityTracker = VelocityTracker.obtain(); 628 } 629 } 630 631 private void recycleVelocityTracker() { 632 if (mVelocityTracker != null) { 633 mVelocityTracker.recycle(); 634 mVelocityTracker = null; 635 } 636 } 637 638 private void initOrResetVelocityTracker() { 639 if (mVelocityTracker == null) { 640 mVelocityTracker = VelocityTracker.obtain(); 641 } else { 642 mVelocityTracker.clear(); 643 } 644 } 645 646 @Override 647 public void computeScroll() { 648 if (mScroller.computeScrollOffset()) { 649 // This is called at drawing time by ViewGroup. 650 int oldX = mScrollX; 651 int oldY = mOwnScrollY; 652 int x = mScroller.getCurrX(); 653 int y = mScroller.getCurrY(); 654 655 if (oldX != x || oldY != y) { 656 final int range = getScrollRange(); 657 final int overscrollMode = getOverScrollMode(); 658 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || 659 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 660 661 overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 662 0, mOverflingDistance, false); 663 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 664 665 if (canOverscroll) { 666 // TODO: Overscroll 667// if (y < 0 && oldY >= 0) { 668// mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 669// } else if (y > range && oldY <= range) { 670// mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 671// } 672 } 673 updateChildren(); 674 } 675 676 // Keep on drawing until the animation has finished. 677 postInvalidateOnAnimation(); 678 } 679 } 680 681 private void customScrollTo(int y) { 682 mOwnScrollY = y; 683 updateChildren(); 684 } 685 686 @Override 687 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 688 // Treat animating scrolls differently; see #computeScroll() for why. 689 if (!mScroller.isFinished()) { 690 final int oldX = mScrollX; 691 final int oldY = mOwnScrollY; 692 mScrollX = scrollX; 693 mOwnScrollY = scrollY; 694 invalidateParentIfNeeded(); 695 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 696 if (clampedY) { 697 mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange()); 698 } 699 updateChildren(); 700 } else { 701 customScrollTo(scrollY); 702 scrollTo(scrollX, mScrollY); 703 } 704 } 705 706 private int getScrollRange() { 707 int scrollRange = 0; 708 ExpandableView firstChild = (ExpandableView) getFirstChildNotGone(); 709 if (firstChild != null) { 710 int contentHeight = getContentHeight(); 711 int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild); 712 713 scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize); 714 if (scrollRange > 0) { 715 View lastChild = getLastChildNotGone(); 716 if (isViewExpanded(lastChild)) { 717 // last child is expanded, so we have to ensure that it can exit the 718 // bottom stack 719 scrollRange += mCollapsedSize + mPaddingBetweenElements; 720 } 721 // We want to at least be able collapse the first item and not ending in a weird 722 // end state. 723 scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize); 724 } 725 } 726 return scrollRange; 727 } 728 729 /** 730 * @return the first child which has visibility unequal to GONE 731 */ 732 private View getFirstChildNotGone() { 733 int childCount = getChildCount(); 734 for (int i = 0; i < childCount; i++) { 735 View child = getChildAt(i); 736 if (child.getVisibility() != View.GONE) { 737 return child; 738 } 739 } 740 return null; 741 } 742 743 /** 744 * @return the last child which has visibility unequal to GONE 745 */ 746 private View getLastChildNotGone() { 747 int childCount = getChildCount(); 748 for (int i = childCount - 1; i >= 0; i--) { 749 View child = getChildAt(i); 750 if (child.getVisibility() != View.GONE) { 751 return child; 752 } 753 } 754 return null; 755 } 756 757 private int getMaxExpandHeight(View view) { 758 if (view instanceof ExpandableNotificationRow) { 759 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 760 return row.getMaximumAllowedExpandHeight(); 761 } 762 return view.getHeight(); 763 } 764 765 private int getContentHeight() { 766 return mContentHeight; 767 } 768 769 private void updateContentHeight() { 770 int height = 0; 771 for (int i = 0; i < getChildCount(); i++) { 772 View child = getChildAt(i); 773 if (child.getVisibility() != View.GONE) { 774 if (height != 0) { 775 // add the padding before this element 776 height += mPaddingBetweenElements; 777 } 778 if (child instanceof ExpandableNotificationRow) { 779 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 780 height += row.getMaximumAllowedExpandHeight(); 781 } else if (child instanceof ExpandableView) { 782 ExpandableView expandableView = (ExpandableView) child; 783 height += expandableView.getActualHeight(); 784 } 785 } 786 } 787 mContentHeight = height + mTopPadding; 788 } 789 790 /** 791 * Fling the scroll view 792 * 793 * @param velocityY The initial velocity in the Y direction. Positive 794 * numbers mean that the finger/cursor is moving down the screen, 795 * which means we want to scroll towards the top. 796 */ 797 private void fling(int velocityY) { 798 if (getChildCount() > 0) { 799 int height = (int) getLayoutHeight(); 800 int bottom = getContentHeight(); 801 802 mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0, 803 Math.max(0, bottom - height), 0, height/2); 804 805 postInvalidateOnAnimation(); 806 } 807 } 808 809 private void endDrag() { 810 setIsBeingDragged(false); 811 812 recycleVelocityTracker(); 813 814 // TODO: Overscroll 815// if (mEdgeGlowTop != null) { 816// mEdgeGlowTop.onRelease(); 817// mEdgeGlowBottom.onRelease(); 818// } 819 } 820 821 @Override 822 public boolean onInterceptTouchEvent(MotionEvent ev) { 823 boolean scrollWantsIt = false; 824 if (!mSwipingInProgress) { 825 scrollWantsIt = onInterceptTouchEventScroll(ev); 826 } 827 boolean swipeWantsIt = false; 828 if (!mIsBeingDragged) { 829 swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); 830 } 831 return swipeWantsIt || scrollWantsIt || 832 super.onInterceptTouchEvent(ev); 833 } 834 835 @Override 836 protected void onViewRemoved(View child) { 837 super.onViewRemoved(child); 838 ((ExpandableView) child).setOnHeightChangedListener(null); 839 mCurrentStackScrollState.removeViewStateForView(child); 840 mStackScrollAlgorithm.notifyChildrenChanged(this); 841 updateScrollStateForRemovedChild(child); 842 if (mIsExpanded) { 843 844 if (!mChildrenToAddAnimated.contains(child)) { 845 // Generate Animations 846 mChildrenToRemoveAnimated.add(child); 847 mNeedsAnimation = true; 848 } else { 849 mChildrenToAddAnimated.remove(child); 850 } 851 } 852 } 853 854 /** 855 * Updates the scroll position when a child was removed 856 * 857 * @param removedChild the removed child 858 */ 859 private void updateScrollStateForRemovedChild(View removedChild) { 860 int startingPosition = getPositionInLinearLayout(removedChild); 861 int childHeight = removedChild.getHeight() + mPaddingBetweenElements; 862 int endPosition = startingPosition + childHeight; 863 if (endPosition <= mOwnScrollY) { 864 // This child is fully scrolled of the top, so we have to deduct its height from the 865 // scrollPosition 866 mOwnScrollY -= childHeight; 867 } else if (startingPosition < mOwnScrollY) { 868 // This child is currently being scrolled into, set the scroll position to the start of 869 // this child 870 mOwnScrollY = startingPosition; 871 } 872 } 873 874 private int getPositionInLinearLayout(View requestedChild) { 875 int position = 0; 876 for (int i = 0; i < getChildCount(); i++) { 877 View child = getChildAt(i); 878 if (child == requestedChild) { 879 return position; 880 } 881 if (child.getVisibility() != View.GONE) { 882 position += child.getHeight(); 883 if (i < getChildCount()-1) { 884 position += mPaddingBetweenElements; 885 } 886 } 887 } 888 return 0; 889 } 890 891 @Override 892 protected void onViewAdded(View child) { 893 super.onViewAdded(child); 894 mStackScrollAlgorithm.notifyChildrenChanged(this); 895 ((ExpandableView) child).setOnHeightChangedListener(this); 896 if (child.getVisibility() != View.GONE) { 897 generateAddAnimation(child); 898 } 899 } 900 901 public void generateAddAnimation(View child) { 902 if (mIsExpanded) { 903 904 // Generate Animations 905 mChildrenToAddAnimated.add(child); 906 mNeedsAnimation = true; 907 } 908 } 909 910 /** 911 * Change the position of child to a new location 912 * 913 * @param child the view to change the position for 914 * @param newIndex the new index 915 */ 916 public void changeViewPosition(View child, int newIndex) { 917 if (child != null && child.getParent() == this) { 918 // TODO: handle this 919 } 920 } 921 922 private void startAnimationToState() { 923 if (mNeedsAnimation) { 924 generateChildHierarchyEvents(); 925 mNeedsAnimation = false; 926 } 927 if (!mAnimationEvents.isEmpty()) { 928 mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState); 929 } else { 930 applyCurrentState(); 931 } 932 } 933 934 private void generateChildHierarchyEvents() { 935 generateChildAdditionEvents(); 936 generateChildRemovalEvents(); 937 generateTopPaddingEvent(); 938 mNeedsAnimation = false; 939 } 940 941 private void generateChildRemovalEvents() { 942 for (View child : mChildrenToRemoveAnimated) { 943 boolean childWasSwipedOut = mSwipedOutViews.contains(child); 944 int animationType = childWasSwipedOut 945 ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT 946 : AnimationEvent.ANIMATION_TYPE_REMOVE; 947 mAnimationEvents.add(new AnimationEvent(child, animationType)); 948 } 949 mSwipedOutViews.clear(); 950 mChildrenToRemoveAnimated.clear(); 951 } 952 953 private void generateChildAdditionEvents() { 954 for (View child : mChildrenToAddAnimated) { 955 mAnimationEvents.add(new AnimationEvent(child, 956 AnimationEvent.ANIMATION_TYPE_ADD)); 957 } 958 mChildrenToAddAnimated.clear(); 959 } 960 961 private void generateTopPaddingEvent() { 962 if (mTopPaddingNeedsAnimation) { 963 mAnimationEvents.add( 964 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED)); 965 } 966 mTopPaddingNeedsAnimation = false; 967 } 968 969 private boolean onInterceptTouchEventScroll(MotionEvent ev) { 970 /* 971 * This method JUST determines whether we want to intercept the motion. 972 * If we return true, onMotionEvent will be called and we do the actual 973 * scrolling there. 974 */ 975 976 /* 977 * Shortcut the most recurring case: the user is in the dragging 978 * state and he is moving his finger. We want to intercept this 979 * motion. 980 */ 981 final int action = ev.getAction(); 982 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 983 return true; 984 } 985 986 /* 987 * Don't try to intercept touch if we can't scroll anyway. 988 */ 989 if (mOwnScrollY == 0 && getScrollRange() == 0) { 990 return false; 991 } 992 993 switch (action & MotionEvent.ACTION_MASK) { 994 case MotionEvent.ACTION_MOVE: { 995 /* 996 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 997 * whether the user has moved far enough from his original down touch. 998 */ 999 1000 /* 1001 * Locally do absolute value. mLastMotionY is set to the y value 1002 * of the down event. 1003 */ 1004 final int activePointerId = mActivePointerId; 1005 if (activePointerId == INVALID_POINTER) { 1006 // If we don't have a valid id, the touch down wasn't on content. 1007 break; 1008 } 1009 1010 final int pointerIndex = ev.findPointerIndex(activePointerId); 1011 if (pointerIndex == -1) { 1012 Log.e(TAG, "Invalid pointerId=" + activePointerId 1013 + " in onInterceptTouchEvent"); 1014 break; 1015 } 1016 1017 final int y = (int) ev.getY(pointerIndex); 1018 final int yDiff = Math.abs(y - mLastMotionY); 1019 if (yDiff > mTouchSlop) { 1020 setIsBeingDragged(true); 1021 mLastMotionY = y; 1022 initVelocityTrackerIfNotExists(); 1023 mVelocityTracker.addMovement(ev); 1024 } 1025 break; 1026 } 1027 1028 case MotionEvent.ACTION_DOWN: { 1029 final int y = (int) ev.getY(); 1030 if (getChildAtPosition(ev.getX(), y) == null) { 1031 setIsBeingDragged(false); 1032 recycleVelocityTracker(); 1033 break; 1034 } 1035 1036 /* 1037 * Remember location of down touch. 1038 * ACTION_DOWN always refers to pointer index 0. 1039 */ 1040 mLastMotionY = y; 1041 mActivePointerId = ev.getPointerId(0); 1042 1043 initOrResetVelocityTracker(); 1044 mVelocityTracker.addMovement(ev); 1045 /* 1046 * If being flinged and user touches the screen, initiate drag; 1047 * otherwise don't. mScroller.isFinished should be false when 1048 * being flinged. 1049 */ 1050 boolean isBeingDragged = !mScroller.isFinished(); 1051 setIsBeingDragged(isBeingDragged); 1052 break; 1053 } 1054 1055 case MotionEvent.ACTION_CANCEL: 1056 case MotionEvent.ACTION_UP: 1057 /* Release the drag */ 1058 setIsBeingDragged(false); 1059 mActivePointerId = INVALID_POINTER; 1060 recycleVelocityTracker(); 1061 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 1062 postInvalidateOnAnimation(); 1063 } 1064 break; 1065 case MotionEvent.ACTION_POINTER_UP: 1066 onSecondaryPointerUp(ev); 1067 break; 1068 } 1069 1070 /* 1071 * The only time we want to intercept motion events is if we are in the 1072 * drag mode. 1073 */ 1074 return mIsBeingDragged; 1075 } 1076 1077 private void setIsBeingDragged(boolean isDragged) { 1078 mIsBeingDragged = isDragged; 1079 if (isDragged) { 1080 requestDisallowInterceptTouchEvent(true); 1081 mSwipeHelper.removeLongPressCallback(); 1082 } 1083 } 1084 1085 @Override 1086 public void onWindowFocusChanged(boolean hasWindowFocus) { 1087 super.onWindowFocusChanged(hasWindowFocus); 1088 if (!hasWindowFocus) { 1089 mSwipeHelper.removeLongPressCallback(); 1090 } 1091 } 1092 1093 @Override 1094 public boolean isScrolledToTop() { 1095 return mOwnScrollY == 0; 1096 } 1097 1098 @Override 1099 public boolean isScrolledToBottom() { 1100 return mOwnScrollY >= getScrollRange(); 1101 } 1102 1103 @Override 1104 public View getHostView() { 1105 return this; 1106 } 1107 1108 public int getEmptyBottomMargin() { 1109 int emptyMargin = mMaxLayoutHeight - mContentHeight; 1110 if (needsHeightAdaption()) { 1111 emptyMargin = emptyMargin - mCollapsedSize - mBottomStackPeekSize; 1112 } 1113 return Math.max(emptyMargin, 0); 1114 } 1115 1116 public void onExpansionStarted() { 1117 mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); 1118 } 1119 1120 public void onExpansionStopped() { 1121 mStackScrollAlgorithm.onExpansionStopped(); 1122 } 1123 1124 private void setIsExpanded(boolean isExpanded) { 1125 mIsExpanded = isExpanded; 1126 mStackScrollAlgorithm.setIsExpanded(isExpanded); 1127 if (!isExpanded) { 1128 mOwnScrollY = 0; 1129 } 1130 } 1131 1132 @Override 1133 public void onHeightChanged(ExpandableView view) { 1134 if (mListenForHeightChanges && !isCurrentlyAnimating()) { 1135 updateContentHeight(); 1136 updateScrollPositionIfNecessary(); 1137 if (mOnHeightChangedListener != null) { 1138 mOnHeightChangedListener.onHeightChanged(view); 1139 } 1140 requestChildrenUpdate(); 1141 } 1142 } 1143 1144 public void setOnHeightChangedListener( 1145 ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { 1146 this.mOnHeightChangedListener = mOnHeightChangedListener; 1147 } 1148 1149 public void onChildAnimationFinished() { 1150 requestChildrenUpdate(); 1151 mAnimationEvents.clear(); 1152 } 1153 1154 private void applyCurrentState() { 1155 mListenForHeightChanges = false; 1156 mCurrentStackScrollState.apply(); 1157 mListenForHeightChanges = true; 1158 if (mListener != null) { 1159 mListener.onChildLocationsChanged(this); 1160 } 1161 } 1162 1163 /** 1164 * A listener that is notified when some child locations might have changed. 1165 */ 1166 public interface OnChildLocationsChangedListener { 1167 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); 1168 } 1169 1170 static class AnimationEvent { 1171 1172 static int ANIMATION_TYPE_ADD = 1; 1173 static int ANIMATION_TYPE_REMOVE = 2; 1174 static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 3; 1175 static int ANIMATION_TYPE_TOP_PADDING_CHANGED = 4; 1176 1177 final long eventStartTime; 1178 final View changingView; 1179 final int animationType; 1180 1181 AnimationEvent(View view, int type) { 1182 eventStartTime = AnimationUtils.currentAnimationTimeMillis(); 1183 changingView = view; 1184 animationType = type; 1185 } 1186 } 1187 1188} 1189