NotificationStackScrollLayout.java revision 068f5929d10a2daf93d6a0aa26e48b1185c36c98
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.Outline; 24import android.graphics.Paint; 25 26import android.util.AttributeSet; 27import android.util.Log; 28 29import android.view.MotionEvent; 30import android.view.VelocityTracker; 31import android.view.View; 32import android.view.ViewConfiguration; 33import android.view.ViewGroup; 34import android.widget.OverScroller; 35 36import com.android.systemui.ExpandHelper; 37import com.android.systemui.R; 38import com.android.systemui.SwipeHelper; 39import com.android.systemui.statusbar.ExpandableNotificationRow; 40import com.android.systemui.statusbar.stack.StackScrollState.ViewState; 41import com.android.systemui.statusbar.policy.ScrollAdapter; 42 43/** 44 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack. 45 */ 46public class NotificationStackScrollLayout extends ViewGroup 47 implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter { 48 49 private static final String TAG = "NotificationStackScrollLayout"; 50 private static final boolean DEBUG = false; 51 52 /** 53 * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}. 54 */ 55 private static final int INVALID_POINTER = -1; 56 57 private SwipeHelper mSwipeHelper; 58 private boolean mSwipingInProgress = true; 59 private int mCurrentStackHeight = Integer.MAX_VALUE; 60 private int mOwnScrollY; 61 private int mMaxLayoutHeight; 62 63 private VelocityTracker mVelocityTracker; 64 private OverScroller mScroller; 65 private int mTouchSlop; 66 private int mMinimumVelocity; 67 private int mMaximumVelocity; 68 private int mOverscrollDistance; 69 private int mOverflingDistance; 70 private boolean mIsBeingDragged; 71 private int mLastMotionY; 72 private int mActivePointerId; 73 74 private int mSidePaddings; 75 private Paint mDebugPaint; 76 private int mBackgroundRoundedRectCornerRadius; 77 private int mContentHeight; 78 private int mCollapsedSize; 79 private int mBottomStackPeekSize; 80 private int mEmptyMarginBottom; 81 private int mPaddingBetweenElements; 82 83 /** 84 * The algorithm which calculates the properties for our children 85 */ 86 private StackScrollAlgorithm mStackScrollAlgorithm; 87 88 /** 89 * The current State this Layout is in 90 */ 91 private final StackScrollState mCurrentStackScrollState = new StackScrollState(this); 92 93 private OnChildLocationsChangedListener mListener; 94 95 public NotificationStackScrollLayout(Context context) { 96 this(context, null); 97 } 98 99 public NotificationStackScrollLayout(Context context, AttributeSet attrs) { 100 this(context, attrs, 0); 101 } 102 103 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) { 104 this(context, attrs, defStyleAttr, 0); 105 } 106 107 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, 108 int defStyleRes) { 109 super(context, attrs, defStyleAttr, defStyleRes); 110 initView(context); 111 if (DEBUG) { 112 setWillNotDraw(false); 113 mDebugPaint = new Paint(); 114 mDebugPaint.setColor(0xffff0000); 115 mDebugPaint.setStrokeWidth(2); 116 mDebugPaint.setStyle(Paint.Style.STROKE); 117 } 118 } 119 120 @Override 121 protected void onDraw(Canvas canvas) { 122 if (DEBUG) { 123 int y = mCollapsedSize; 124 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 125 y = (int) (getLayoutHeight() - mBottomStackPeekSize - mCollapsedSize); 126 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 127 y = (int) getLayoutHeight(); 128 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 129 } 130 } 131 132 private void initView(Context context) { 133 mScroller = new OverScroller(getContext()); 134 setFocusable(true); 135 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 136 final ViewConfiguration configuration = ViewConfiguration.get(context); 137 mTouchSlop = configuration.getScaledTouchSlop(); 138 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 139 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 140 mOverscrollDistance = configuration.getScaledOverscrollDistance(); 141 mOverflingDistance = configuration.getScaledOverflingDistance(); 142 float densityScale = getResources().getDisplayMetrics().density; 143 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 144 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); 145 146 mSidePaddings = context.getResources() 147 .getDimensionPixelSize(R.dimen.notification_side_padding); 148 mBackgroundRoundedRectCornerRadius = context.getResources() 149 .getDimensionPixelSize( 150 com.android.internal.R.dimen.notification_quantum_rounded_rect_radius); 151 mCollapsedSize = context.getResources() 152 .getDimensionPixelSize(R.dimen.notification_row_min_height); 153 mBottomStackPeekSize = context.getResources() 154 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 155 mEmptyMarginBottom = context.getResources().getDimensionPixelSize( 156 R.dimen.notification_stack_margin_bottom); 157 // currently the padding is in the elements themself 158 mPaddingBetweenElements = 0; 159 mStackScrollAlgorithm = new StackScrollAlgorithm(context); 160 } 161 162 @Override 163 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 164 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 165 int mode = MeasureSpec.getMode(widthMeasureSpec); 166 int size = MeasureSpec.getSize(widthMeasureSpec); 167 int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode); 168 measureChildren(childMeasureSpec, heightMeasureSpec); 169 } 170 171 @Override 172 protected void onLayout(boolean changed, int l, int t, int r, int b) { 173 174 // we layout all our children centered on the top 175 float centerX = getWidth() / 2.0f; 176 for (int i = 0; i < getChildCount(); i++) { 177 View child = getChildAt(i); 178 float width = child.getMeasuredWidth(); 179 float height = child.getMeasuredHeight(); 180 int oldWidth = child.getWidth(); 181 int oldHeight = child.getHeight(); 182 child.layout((int) (centerX - width / 2.0f), 183 0, 184 (int) (centerX + width / 2.0f), 185 (int) height); 186 updateChildOutline(child, width, height, oldWidth, oldHeight); 187 } 188 setMaxLayoutHeight(getHeight() - mEmptyMarginBottom); 189 updateScrollPositionIfNecessary(); 190 updateChildren(); 191 updateContentHeight(); 192 } 193 194 public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) { 195 mListener = listener; 196 } 197 198 /** 199 * Returns the location the given child is currently rendered at. 200 * 201 * @param child the child to get the location for 202 * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants 203 */ 204 public int getChildLocation(View child) { 205 ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child); 206 if (childViewState == null) { 207 return ViewState.LOCATION_UNKNOWN; 208 } 209 return childViewState.location; 210 } 211 212 private void setMaxLayoutHeight(int maxLayoutHeight) { 213 mMaxLayoutHeight = maxLayoutHeight; 214 updateAlgorithmHeight(); 215 } 216 217 private void updateAlgorithmHeight() { 218 mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight()); 219 } 220 221 /** 222 * Updates the children views according to the stack scroll algorithm. Call this whenever 223 * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout. 224 */ 225 private void updateChildren() { 226 if (!isCurrentlyAnimating()) { 227 mCurrentStackScrollState.setScrollY(mOwnScrollY); 228 mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState); 229 mCurrentStackScrollState.apply(); 230 mOwnScrollY = mCurrentStackScrollState.getScrollY(); 231 if (mListener != null) { 232 mListener.onChildLocationsChanged(this); 233 } 234 } else { 235 // TODO: handle animation 236 } 237 } 238 239 private boolean isCurrentlyAnimating() { 240 return false; 241 } 242 243 private void updateChildOutline(View child, 244 float width, 245 float height, 246 int oldWidth, 247 int oldHeight) { 248 // The children currently have paddings inside themselfs because of the expansion 249 // visualization. In order for the shadows to work correctly we have to set the correct 250 // outline. 251 View container = child.findViewById(R.id.container); 252 if (container != null && (oldWidth != width || oldHeight != height)) { 253 Outline outline = getOutlineForSize(container.getLeft(), 254 container.getTop(), 255 container.getWidth(), 256 container.getHeight()); 257 child.setOutline(outline); 258 } 259 } 260 261 private Outline getOutlineForSize(int leftInset, int topInset, int width, int height) { 262 Outline result = new Outline(); 263 result.setRoundRect(leftInset, topInset, leftInset + width, topInset + height, 264 mBackgroundRoundedRectCornerRadius); 265 return result; 266 } 267 268 private void updateScrollPositionIfNecessary() { 269 int scrollRange = getScrollRange(); 270 if (scrollRange < mOwnScrollY) { 271 mOwnScrollY = scrollRange; 272 } 273 } 274 275 public void setCurrentStackHeight(int currentStackHeight) { 276 this.mCurrentStackHeight = currentStackHeight; 277 updateAlgorithmHeight(); 278 updateChildren(); 279 } 280 281 /** 282 * Get the current height of the view. This is at most the msize of the view given by a the 283 * layout but it can also be made smaller by setting {@link #mCurrentStackHeight} 284 * 285 * @return either the layout height or the externally defined height, whichever is smaller 286 */ 287 private float getLayoutHeight() { 288 return Math.min(mMaxLayoutHeight, mCurrentStackHeight); 289 } 290 291 public int getItemHeight() { 292 return mCollapsedSize; 293 } 294 295 public int getBottomStackPeekSize() { 296 return mBottomStackPeekSize; 297 } 298 299 public void setLongPressListener(View.OnLongClickListener listener) { 300 mSwipeHelper.setLongPressListener(listener); 301 } 302 303 public void onChildDismissed(View v) { 304 if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); 305 final View veto = v.findViewById(R.id.veto); 306 if (veto != null && veto.getVisibility() != View.GONE) { 307 veto.performClick(); 308 } 309 setSwipingInProgress(false); 310 } 311 312 public void onBeginDrag(View v) { 313 setSwipingInProgress(true); 314 } 315 316 public void onDragCancelled(View v) { 317 setSwipingInProgress(false); 318 } 319 320 public View getChildAtPosition(MotionEvent ev) { 321 return getChildAtPosition(ev.getX(), ev.getY()); 322 } 323 324 public View getChildAtRawPosition(float touchX, float touchY) { 325 int[] location = new int[2]; 326 getLocationOnScreen(location); 327 return getChildAtPosition(touchX - location[0],touchY - location[1]); 328 } 329 330 public View getChildAtPosition(float touchX, float touchY) { 331 // find the view under the pointer, accounting for GONE views 332 final int count = getChildCount(); 333 for (int childIdx = 0; childIdx < count; childIdx++) { 334 View slidingChild = getChildAt(childIdx); 335 if (slidingChild.getVisibility() == GONE) { 336 continue; 337 } 338 float top = slidingChild.getTranslationY(); 339 float bottom = top + slidingChild.getMeasuredHeight(); 340 int left = slidingChild.getLeft(); 341 int right = slidingChild.getRight(); 342 343 if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { 344 return slidingChild; 345 } 346 } 347 return null; 348 } 349 350 public boolean canChildBeExpanded(View v) { 351 return v instanceof ExpandableNotificationRow 352 && ((ExpandableNotificationRow) v).isExpandable(); 353 } 354 355 public void setUserExpandedChild(View v, boolean userExpanded) { 356 if (v instanceof ExpandableNotificationRow) { 357 ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); 358 } 359 } 360 361 public void setUserLockedChild(View v, boolean userLocked) { 362 if (v instanceof ExpandableNotificationRow) { 363 ((ExpandableNotificationRow) v).setUserLocked(userLocked); 364 } 365 } 366 367 public View getChildContentView(View v) { 368 return v; 369 } 370 371 public boolean canChildBeDismissed(View v) { 372 final View veto = v.findViewById(R.id.veto); 373 return (veto != null && veto.getVisibility() != View.GONE); 374 } 375 376 private void setSwipingInProgress(boolean isSwiped) { 377 mSwipingInProgress = isSwiped; 378 if(isSwiped) { 379 requestDisallowInterceptTouchEvent(true); 380 } 381 } 382 383 @Override 384 protected void onConfigurationChanged(Configuration newConfig) { 385 super.onConfigurationChanged(newConfig); 386 float densityScale = getResources().getDisplayMetrics().density; 387 mSwipeHelper.setDensityScale(densityScale); 388 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 389 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 390 initView(getContext()); 391 } 392 393 public void dismissRowAnimated(View child, int vel) { 394 mSwipeHelper.dismissChild(child, vel); 395 } 396 397 @Override 398 public boolean onTouchEvent(MotionEvent ev) { 399 boolean scrollerWantsIt = false; 400 if (!mSwipingInProgress) { 401 scrollerWantsIt = onScrollTouch(ev); 402 } 403 boolean horizontalSwipeWantsIt = false; 404 if (!mIsBeingDragged) { 405 horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); 406 } 407 return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev); 408 } 409 410 private boolean onScrollTouch(MotionEvent ev) { 411 initVelocityTrackerIfNotExists(); 412 mVelocityTracker.addMovement(ev); 413 414 final int action = ev.getAction(); 415 416 switch (action & MotionEvent.ACTION_MASK) { 417 case MotionEvent.ACTION_DOWN: { 418 if (getChildCount() == 0) { 419 return false; 420 } 421 boolean isBeingDragged = !mScroller.isFinished(); 422 setIsBeingDragged(isBeingDragged); 423 424 /* 425 * If being flinged and user touches, stop the fling. isFinished 426 * will be false if being flinged. 427 */ 428 if (!mScroller.isFinished()) { 429 mScroller.abortAnimation(); 430 } 431 432 // Remember where the motion event started 433 mLastMotionY = (int) ev.getY(); 434 mActivePointerId = ev.getPointerId(0); 435 break; 436 } 437 case MotionEvent.ACTION_MOVE: 438 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 439 if (activePointerIndex == -1) { 440 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 441 break; 442 } 443 444 final int y = (int) ev.getY(activePointerIndex); 445 int deltaY = mLastMotionY - y; 446 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 447 setIsBeingDragged(true); 448 if (deltaY > 0) { 449 deltaY -= mTouchSlop; 450 } else { 451 deltaY += mTouchSlop; 452 } 453 } 454 if (mIsBeingDragged) { 455 // Scroll to follow the motion event 456 mLastMotionY = y; 457 458 final int oldX = mScrollX; 459 final int oldY = mOwnScrollY; 460 final int range = getScrollRange(); 461 final int overscrollMode = getOverScrollMode(); 462 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || 463 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 464 465 // Calling overScrollBy will call onOverScrolled, which 466 // calls onScrollChanged if applicable. 467 if (overScrollBy(0, deltaY, 0, mOwnScrollY, 468 0, range, 0, mOverscrollDistance, true)) { 469 // Break our velocity if we hit a scroll barrier. 470 mVelocityTracker.clear(); 471 } 472 // TODO: Overscroll 473// if (canOverscroll) { 474// final int pulledToY = oldY + deltaY; 475// if (pulledToY < 0) { 476// mEdgeGlowTop.onPull((float) deltaY / getHeight()); 477// if (!mEdgeGlowBottom.isFinished()) { 478// mEdgeGlowBottom.onRelease(); 479// } 480// } else if (pulledToY > range) { 481// mEdgeGlowBottom.onPull((float) deltaY / getHeight()); 482// if (!mEdgeGlowTop.isFinished()) { 483// mEdgeGlowTop.onRelease(); 484// } 485// } 486// if (mEdgeGlowTop != null 487// && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){ 488// postInvalidateOnAnimation(); 489// } 490// } 491 } 492 break; 493 case MotionEvent.ACTION_UP: 494 if (mIsBeingDragged) { 495 final VelocityTracker velocityTracker = mVelocityTracker; 496 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 497 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 498 499 if (getChildCount() > 0) { 500 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 501 fling(-initialVelocity); 502 } else { 503 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, 504 getScrollRange())) { 505 postInvalidateOnAnimation(); 506 } 507 } 508 } 509 510 mActivePointerId = INVALID_POINTER; 511 endDrag(); 512 } 513 break; 514 case MotionEvent.ACTION_CANCEL: 515 if (mIsBeingDragged && getChildCount() > 0) { 516 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 517 postInvalidateOnAnimation(); 518 } 519 mActivePointerId = INVALID_POINTER; 520 endDrag(); 521 } 522 break; 523 case MotionEvent.ACTION_POINTER_DOWN: { 524 final int index = ev.getActionIndex(); 525 mLastMotionY = (int) ev.getY(index); 526 mActivePointerId = ev.getPointerId(index); 527 break; 528 } 529 case MotionEvent.ACTION_POINTER_UP: 530 onSecondaryPointerUp(ev); 531 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 532 break; 533 } 534 return true; 535 } 536 537 private void onSecondaryPointerUp(MotionEvent ev) { 538 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 539 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 540 final int pointerId = ev.getPointerId(pointerIndex); 541 if (pointerId == mActivePointerId) { 542 // This was our active pointer going up. Choose a new 543 // active pointer and adjust accordingly. 544 // TODO: Make this decision more intelligent. 545 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 546 mLastMotionY = (int) ev.getY(newPointerIndex); 547 mActivePointerId = ev.getPointerId(newPointerIndex); 548 if (mVelocityTracker != null) { 549 mVelocityTracker.clear(); 550 } 551 } 552 } 553 554 private void initVelocityTrackerIfNotExists() { 555 if (mVelocityTracker == null) { 556 mVelocityTracker = VelocityTracker.obtain(); 557 } 558 } 559 560 private void recycleVelocityTracker() { 561 if (mVelocityTracker != null) { 562 mVelocityTracker.recycle(); 563 mVelocityTracker = null; 564 } 565 } 566 567 private void initOrResetVelocityTracker() { 568 if (mVelocityTracker == null) { 569 mVelocityTracker = VelocityTracker.obtain(); 570 } else { 571 mVelocityTracker.clear(); 572 } 573 } 574 575 @Override 576 public void computeScroll() { 577 if (mScroller.computeScrollOffset()) { 578 // This is called at drawing time by ViewGroup. 579 int oldX = mScrollX; 580 int oldY = mOwnScrollY; 581 int x = mScroller.getCurrX(); 582 int y = mScroller.getCurrY(); 583 584 if (oldX != x || oldY != y) { 585 final int range = getScrollRange(); 586 final int overscrollMode = getOverScrollMode(); 587 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || 588 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 589 590 overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 591 0, mOverflingDistance, false); 592 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 593 594 if (canOverscroll) { 595 // TODO: Overscroll 596// if (y < 0 && oldY >= 0) { 597// mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 598// } else if (y > range && oldY <= range) { 599// mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 600// } 601 } 602 updateChildren(); 603 } 604 605 // Keep on drawing until the animation has finished. 606 postInvalidateOnAnimation(); 607 } 608 } 609 610 public void customScrollBy(int y) { 611 mOwnScrollY += y; 612 updateChildren(); 613 } 614 615 public void customScrollTo(int y) { 616 mOwnScrollY = y; 617 updateChildren(); 618 } 619 620 @Override 621 protected void onOverScrolled(int scrollX, int scrollY, 622 boolean clampedX, boolean clampedY) { 623 // Treat animating scrolls differently; see #computeScroll() for why. 624 if (!mScroller.isFinished()) { 625 final int oldX = mScrollX; 626 final int oldY = mOwnScrollY; 627 mScrollX = scrollX; 628 mOwnScrollY = scrollY; 629 invalidateParentIfNeeded(); 630 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 631 if (clampedY) { 632 mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange()); 633 } 634 updateChildren(); 635 } else { 636 customScrollTo(scrollY); 637 scrollTo(scrollX, mScrollY); 638 } 639 } 640 641 private int getScrollRange() { 642 int scrollRange = 0; 643 if (getChildCount() > 0) { 644 int contentHeight = getContentHeight(); 645 scrollRange = Math.max(0, 646 contentHeight - mMaxLayoutHeight + mBottomStackPeekSize); 647 } 648 return scrollRange; 649 } 650 651 private int getContentHeight() { 652 return mContentHeight; 653 } 654 655 private void updateContentHeight() { 656 int height = 0; 657 for (int i = 0; i < getChildCount(); i++) { 658 View child = getChildAt(i); 659 height += child.getHeight(); 660 if (i < getChildCount()-1) { 661 height += mPaddingBetweenElements; 662 } 663 } 664 mContentHeight = height; 665 } 666 667 /** 668 * Fling the scroll view 669 * 670 * @param velocityY The initial velocity in the Y direction. Positive 671 * numbers mean that the finger/cursor is moving down the screen, 672 * which means we want to scroll towards the top. 673 */ 674 private void fling(int velocityY) { 675 if (getChildCount() > 0) { 676 int height = (int) getLayoutHeight(); 677 int bottom = getContentHeight(); 678 679 mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0, 680 Math.max(0, bottom - height), 0, height/2); 681 682 postInvalidateOnAnimation(); 683 } 684 } 685 686 private void endDrag() { 687 setIsBeingDragged(false); 688 689 recycleVelocityTracker(); 690 691 // TODO: Overscroll 692// if (mEdgeGlowTop != null) { 693// mEdgeGlowTop.onRelease(); 694// mEdgeGlowBottom.onRelease(); 695// } 696 } 697 698 @Override 699 public boolean onInterceptTouchEvent(MotionEvent ev) { 700 boolean scrollWantsIt = false; 701 if (!mSwipingInProgress) { 702 scrollWantsIt = onInterceptTouchEventScroll(ev); 703 } 704 boolean swipeWantsIt = false; 705 if (!mIsBeingDragged) { 706 swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); 707 } 708 return swipeWantsIt || scrollWantsIt || 709 super.onInterceptTouchEvent(ev); 710 } 711 712 @Override 713 protected void onViewRemoved(View child) { 714 super.onViewRemoved(child); 715 mCurrentStackScrollState.removeViewStateForView(child); 716 } 717 718 private boolean onInterceptTouchEventScroll(MotionEvent ev) { 719 /* 720 * This method JUST determines whether we want to intercept the motion. 721 * If we return true, onMotionEvent will be called and we do the actual 722 * scrolling there. 723 */ 724 725 /* 726 * Shortcut the most recurring case: the user is in the dragging 727 * state and he is moving his finger. We want to intercept this 728 * motion. 729 */ 730 final int action = ev.getAction(); 731 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 732 return true; 733 } 734 735 /* 736 * Don't try to intercept touch if we can't scroll anyway. 737 */ 738 if (mOwnScrollY == 0 && getScrollRange() == 0) { 739 return false; 740 } 741 742 switch (action & MotionEvent.ACTION_MASK) { 743 case MotionEvent.ACTION_MOVE: { 744 /* 745 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 746 * whether the user has moved far enough from his original down touch. 747 */ 748 749 /* 750 * Locally do absolute value. mLastMotionY is set to the y value 751 * of the down event. 752 */ 753 final int activePointerId = mActivePointerId; 754 if (activePointerId == INVALID_POINTER) { 755 // If we don't have a valid id, the touch down wasn't on content. 756 break; 757 } 758 759 final int pointerIndex = ev.findPointerIndex(activePointerId); 760 if (pointerIndex == -1) { 761 Log.e(TAG, "Invalid pointerId=" + activePointerId 762 + " in onInterceptTouchEvent"); 763 break; 764 } 765 766 final int y = (int) ev.getY(pointerIndex); 767 final int yDiff = Math.abs(y - mLastMotionY); 768 if (yDiff > mTouchSlop) { 769 setIsBeingDragged(true); 770 mLastMotionY = y; 771 initVelocityTrackerIfNotExists(); 772 mVelocityTracker.addMovement(ev); 773 } 774 break; 775 } 776 777 case MotionEvent.ACTION_DOWN: { 778 final int y = (int) ev.getY(); 779 if (getChildAtPosition(ev.getX(), y) == null) { 780 setIsBeingDragged(false); 781 recycleVelocityTracker(); 782 break; 783 } 784 785 /* 786 * Remember location of down touch. 787 * ACTION_DOWN always refers to pointer index 0. 788 */ 789 mLastMotionY = y; 790 mActivePointerId = ev.getPointerId(0); 791 792 initOrResetVelocityTracker(); 793 mVelocityTracker.addMovement(ev); 794 /* 795 * If being flinged and user touches the screen, initiate drag; 796 * otherwise don't. mScroller.isFinished should be false when 797 * being flinged. 798 */ 799 boolean isBeingDragged = !mScroller.isFinished(); 800 setIsBeingDragged(isBeingDragged); 801 break; 802 } 803 804 case MotionEvent.ACTION_CANCEL: 805 case MotionEvent.ACTION_UP: 806 /* Release the drag */ 807 setIsBeingDragged(false); 808 mActivePointerId = INVALID_POINTER; 809 recycleVelocityTracker(); 810 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 811 postInvalidateOnAnimation(); 812 } 813 break; 814 case MotionEvent.ACTION_POINTER_UP: 815 onSecondaryPointerUp(ev); 816 break; 817 } 818 819 /* 820 * The only time we want to intercept motion events is if we are in the 821 * drag mode. 822 */ 823 return mIsBeingDragged; 824 } 825 826 private void setIsBeingDragged(boolean isDragged) { 827 mIsBeingDragged = isDragged; 828 if (isDragged) { 829 requestDisallowInterceptTouchEvent(true); 830 mSwipeHelper.removeLongPressCallback(); 831 } 832 } 833 834 @Override 835 public void onWindowFocusChanged(boolean hasWindowFocus) { 836 super.onWindowFocusChanged(hasWindowFocus); 837 if (!hasWindowFocus) { 838 mSwipeHelper.removeLongPressCallback(); 839 } 840 } 841 842 @Override 843 public boolean isScrolledToTop() { 844 return mOwnScrollY == 0; 845 } 846 847 @Override 848 public boolean isScrolledToBottom() { 849 return mOwnScrollY >= getScrollRange(); 850 } 851 852 @Override 853 public View getHostView() { 854 return this; 855 } 856 857 public int getEmptyBottomMargin() { 858 return Math.max(getHeight() - mContentHeight, 0); 859 } 860 861 /** 862 * A listener that is notified when some child locations might have changed. 863 */ 864 public interface OnChildLocationsChangedListener { 865 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); 866 } 867} 868