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