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