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