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