NotificationStackScrollLayout.java revision 8d9ff9c2c66bc1d3b92eb6992d58599ff80ed6dc
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 private static final float RUBBER_BAND_FACTOR = 0.35f; 57 58 /** 59 * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}. 60 */ 61 private static final int INVALID_POINTER = -1; 62 63 private SwipeHelper mSwipeHelper; 64 private boolean mSwipingInProgress; 65 private int mCurrentStackHeight = Integer.MAX_VALUE; 66 private int mOwnScrollY; 67 private int mMaxLayoutHeight; 68 69 private VelocityTracker mVelocityTracker; 70 private OverScroller mScroller; 71 private int mTouchSlop; 72 private int mMinimumVelocity; 73 private int mMaximumVelocity; 74 private int mOverflingDistance; 75 private float mMaxOverScroll; 76 private boolean mIsBeingDragged; 77 private int mLastMotionY; 78 private int mActivePointerId; 79 80 private int mSidePaddings; 81 private Paint mDebugPaint; 82 private int mContentHeight; 83 private int mCollapsedSize; 84 private int mBottomStackSlowDownHeight; 85 private int mBottomStackPeekSize; 86 private int mEmptyMarginBottom; 87 private int mPaddingBetweenElements; 88 private int mPaddingBetweenElementsDimmed; 89 private int mPaddingBetweenElementsNormal; 90 private int mTopPadding; 91 92 /** 93 * The algorithm which calculates the properties for our children 94 */ 95 private StackScrollAlgorithm mStackScrollAlgorithm; 96 97 /** 98 * The current State this Layout is in 99 */ 100 private StackScrollState mCurrentStackScrollState = new StackScrollState(this); 101 private AmbientState mAmbientState = new AmbientState(); 102 private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>(); 103 private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>(); 104 private ArrayList<View> mSnappedBackChildren = new ArrayList<View>(); 105 private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>(); 106 private ArrayList<AnimationEvent> mAnimationEvents 107 = new ArrayList<AnimationEvent>(); 108 private ArrayList<View> mSwipedOutViews = new ArrayList<View>(); 109 private final StackStateAnimator mStateAnimator = new StackStateAnimator(this); 110 111 /** 112 * The raw amount of the overScroll on the top, which is not rubber-banded. 113 */ 114 private float mOverScrolledTopPixels; 115 116 /** 117 * The raw amount of the overScroll on the bottom, which is not rubber-banded. 118 */ 119 private float mOverScrolledBottomPixels; 120 121 private OnChildLocationsChangedListener mListener; 122 private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; 123 private boolean mNeedsAnimation; 124 private boolean mTopPaddingNeedsAnimation; 125 private boolean mDimmedNeedsAnimation; 126 private boolean mActivateNeedsAnimation; 127 private boolean mIsExpanded = true; 128 private boolean mChildrenUpdateRequested; 129 private ViewTreeObserver.OnPreDrawListener mChildrenUpdater 130 = new ViewTreeObserver.OnPreDrawListener() { 131 @Override 132 public boolean onPreDraw() { 133 updateChildren(); 134 mChildrenUpdateRequested = false; 135 getViewTreeObserver().removeOnPreDrawListener(this); 136 return true; 137 } 138 }; 139 140 public NotificationStackScrollLayout(Context context) { 141 this(context, null); 142 } 143 144 public NotificationStackScrollLayout(Context context, AttributeSet attrs) { 145 this(context, attrs, 0); 146 } 147 148 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) { 149 this(context, attrs, defStyleAttr, 0); 150 } 151 152 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, 153 int defStyleRes) { 154 super(context, attrs, defStyleAttr, defStyleRes); 155 initView(context); 156 if (DEBUG) { 157 setWillNotDraw(false); 158 mDebugPaint = new Paint(); 159 mDebugPaint.setColor(0xffff0000); 160 mDebugPaint.setStrokeWidth(2); 161 mDebugPaint.setStyle(Paint.Style.STROKE); 162 } 163 } 164 165 @Override 166 protected void onDraw(Canvas canvas) { 167 if (DEBUG) { 168 int y = mCollapsedSize; 169 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 170 y = (int) (getLayoutHeight() - mBottomStackPeekSize 171 - mBottomStackSlowDownHeight); 172 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 173 y = (int) (getLayoutHeight() - mBottomStackPeekSize); 174 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 175 y = (int) getLayoutHeight(); 176 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 177 } 178 } 179 180 private void initView(Context context) { 181 mScroller = new OverScroller(getContext()); 182 setFocusable(true); 183 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 184 setClipChildren(false); 185 final ViewConfiguration configuration = ViewConfiguration.get(context); 186 mTouchSlop = configuration.getScaledTouchSlop(); 187 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 188 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 189 mOverflingDistance = configuration.getScaledOverflingDistance(); 190 float densityScale = getResources().getDisplayMetrics().density; 191 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 192 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); 193 194 mSidePaddings = context.getResources() 195 .getDimensionPixelSize(R.dimen.notification_side_padding); 196 mCollapsedSize = context.getResources() 197 .getDimensionPixelSize(R.dimen.notification_min_height); 198 mBottomStackPeekSize = context.getResources() 199 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 200 mEmptyMarginBottom = context.getResources().getDimensionPixelSize( 201 R.dimen.notification_stack_margin_bottom); 202 mStackScrollAlgorithm = new StackScrollAlgorithm(context); 203 mPaddingBetweenElementsDimmed = context.getResources() 204 .getDimensionPixelSize(R.dimen.notification_padding_dimmed); 205 mPaddingBetweenElementsNormal = context.getResources() 206 .getDimensionPixelSize(R.dimen.notification_padding); 207 updatePadding(false); 208 } 209 210 private void updatePadding(boolean dimmed) { 211 mPaddingBetweenElements = dimmed 212 ? mPaddingBetweenElementsDimmed 213 : mPaddingBetweenElementsNormal; 214 mBottomStackSlowDownHeight = mStackScrollAlgorithm.getBottomStackSlowDownLength(); 215 updateContentHeight(); 216 } 217 218 @Override 219 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 220 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 221 int mode = MeasureSpec.getMode(widthMeasureSpec); 222 int size = MeasureSpec.getSize(widthMeasureSpec); 223 int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode); 224 measureChildren(childMeasureSpec, heightMeasureSpec); 225 } 226 227 @Override 228 protected void onLayout(boolean changed, int l, int t, int r, int b) { 229 230 // we layout all our children centered on the top 231 float centerX = getWidth() / 2.0f; 232 for (int i = 0; i < getChildCount(); i++) { 233 View child = getChildAt(i); 234 float width = child.getMeasuredWidth(); 235 float height = child.getMeasuredHeight(); 236 child.layout((int) (centerX - width / 2.0f), 237 0, 238 (int) (centerX + width / 2.0f), 239 (int) height); 240 } 241 setMaxLayoutHeight(getHeight() - mEmptyMarginBottom); 242 updateContentHeight(); 243 updateScrollPositionIfNecessary(); 244 requestChildrenUpdate(); 245 } 246 247 public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) { 248 mListener = listener; 249 } 250 251 /** 252 * Returns the location the given child is currently rendered at. 253 * 254 * @param child the child to get the location for 255 * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants 256 */ 257 public int getChildLocation(View child) { 258 ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child); 259 if (childViewState == null) { 260 return ViewState.LOCATION_UNKNOWN; 261 } 262 return childViewState.location; 263 } 264 265 private void setMaxLayoutHeight(int maxLayoutHeight) { 266 mMaxLayoutHeight = maxLayoutHeight; 267 updateAlgorithmHeightAndPadding(); 268 } 269 270 private void updateAlgorithmHeightAndPadding() { 271 mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight()); 272 mStackScrollAlgorithm.setTopPadding(mTopPadding); 273 } 274 275 /** 276 * @return whether the height of the layout needs to be adapted, in order to ensure that the 277 * last child is not in the bottom stack. 278 */ 279 private boolean needsHeightAdaption() { 280 View lastChild = getLastChildNotGone(); 281 View firstChild = getFirstChildNotGone(); 282 boolean isLastChildExpanded = isViewExpanded(lastChild); 283 return isLastChildExpanded && lastChild != firstChild; 284 } 285 286 private boolean isViewExpanded(View view) { 287 if (view != null) { 288 ExpandableView expandView = (ExpandableView) view; 289 return expandView.getActualHeight() > mCollapsedSize; 290 } 291 return false; 292 } 293 294 /** 295 * Updates the children views according to the stack scroll algorithm. Call this whenever 296 * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout. 297 */ 298 private void updateChildren() { 299 mAmbientState.setScrollY(mOwnScrollY); 300 mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState); 301 if (!isCurrentlyAnimating() && !mNeedsAnimation) { 302 applyCurrentState(); 303 } else { 304 startAnimationToState(); 305 } 306 } 307 308 private void requestChildrenUpdate() { 309 if (!mChildrenUpdateRequested) { 310 getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater); 311 mChildrenUpdateRequested = true; 312 invalidate(); 313 } 314 } 315 316 private boolean isCurrentlyAnimating() { 317 return mStateAnimator.isRunning(); 318 } 319 320 private void updateScrollPositionIfNecessary() { 321 int scrollRange = getScrollRange(); 322 if (scrollRange < mOwnScrollY) { 323 mOwnScrollY = scrollRange; 324 } 325 } 326 327 public int getTopPadding() { 328 return mTopPadding; 329 } 330 331 public void setTopPadding(int topPadding, boolean animate) { 332 if (mTopPadding != topPadding) { 333 mTopPadding = topPadding; 334 updateAlgorithmHeightAndPadding(); 335 updateContentHeight(); 336 if (animate) { 337 mTopPaddingNeedsAnimation = true; 338 mNeedsAnimation = true; 339 } 340 requestChildrenUpdate(); 341 if (mOnHeightChangedListener != null) { 342 mOnHeightChangedListener.onHeightChanged(null); 343 } 344 } 345 } 346 347 /** 348 * Update the height of the stack to a new height. 349 * 350 * @param height the new height of the stack 351 */ 352 public void setStackHeight(float height) { 353 setIsExpanded(height > 0.0f); 354 int newStackHeight = (int) height; 355 int itemHeight = getItemHeight(); 356 int bottomStackPeekSize = mBottomStackPeekSize; 357 int minStackHeight = itemHeight + bottomStackPeekSize; 358 int stackHeight; 359 if (newStackHeight - mTopPadding >= minStackHeight) { 360 setTranslationY(0); 361 stackHeight = newStackHeight; 362 } else { 363 364 // We did not reach the position yet where we actually start growing, 365 // so we translate the stack upwards. 366 int translationY = (newStackHeight - minStackHeight); 367 // A slight parallax effect is introduced in order for the stack to catch up with 368 // the top card. 369 float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight; 370 partiallyThere = Math.max(0, partiallyThere); 371 translationY += (1 - partiallyThere) * bottomStackPeekSize; 372 setTranslationY(translationY - mTopPadding); 373 stackHeight = (int) (height - (translationY - mTopPadding)); 374 } 375 if (stackHeight != mCurrentStackHeight) { 376 mCurrentStackHeight = stackHeight; 377 updateAlgorithmHeightAndPadding(); 378 requestChildrenUpdate(); 379 } 380 } 381 382 /** 383 * Get the current height of the view. This is at most the msize of the view given by a the 384 * layout but it can also be made smaller by setting {@link #mCurrentStackHeight} 385 * 386 * @return either the layout height or the externally defined height, whichever is smaller 387 */ 388 private int getLayoutHeight() { 389 return Math.min(mMaxLayoutHeight, mCurrentStackHeight); 390 } 391 392 public int getItemHeight() { 393 return mCollapsedSize; 394 } 395 396 public int getBottomStackPeekSize() { 397 return mBottomStackPeekSize; 398 } 399 400 public void setLongPressListener(View.OnLongClickListener listener) { 401 mSwipeHelper.setLongPressListener(listener); 402 } 403 404 public void onChildDismissed(View v) { 405 if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); 406 final View veto = v.findViewById(R.id.veto); 407 if (veto != null && veto.getVisibility() != View.GONE) { 408 veto.performClick(); 409 } 410 setSwipingInProgress(false); 411 if (mDragAnimPendingChildren.contains(v)) { 412 // We start the swipe and finish it in the same frame, we don't want any animation 413 // for the drag 414 mDragAnimPendingChildren.remove(v); 415 } 416 mSwipedOutViews.add(v); 417 mAmbientState.onDragFinished(v); 418 } 419 420 @Override 421 public void onChildSnappedBack(View animView) { 422 mAmbientState.onDragFinished(animView); 423 if (!mDragAnimPendingChildren.contains(animView)) { 424 mSnappedBackChildren.add(animView); 425 requestChildrenUpdate(); 426 mNeedsAnimation = true; 427 } else { 428 // We start the swipe and snap back in the same frame, we don't want any animation 429 mDragAnimPendingChildren.remove(animView); 430 } 431 } 432 433 public void onBeginDrag(View v) { 434 setSwipingInProgress(true); 435 mDragAnimPendingChildren.add(v); 436 mAmbientState.onBeginDrag(v); 437 requestChildrenUpdate(); 438 mNeedsAnimation = true; 439 } 440 441 public void onDragCancelled(View v) { 442 setSwipingInProgress(false); 443 } 444 445 public View getChildAtPosition(MotionEvent ev) { 446 return getChildAtPosition(ev.getX(), ev.getY()); 447 } 448 449 public ExpandableView getChildAtRawPosition(float touchX, float touchY) { 450 int[] location = new int[2]; 451 getLocationOnScreen(location); 452 return getChildAtPosition(touchX - location[0], touchY - location[1]); 453 } 454 455 public ExpandableView getChildAtPosition(float touchX, float touchY) { 456 // find the view under the pointer, accounting for GONE views 457 final int count = getChildCount(); 458 for (int childIdx = 0; childIdx < count; childIdx++) { 459 ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); 460 if (slidingChild.getVisibility() == GONE) { 461 continue; 462 } 463 float top = slidingChild.getTranslationY(); 464 float bottom = top + slidingChild.getActualHeight(); 465 int left = slidingChild.getLeft(); 466 int right = slidingChild.getRight(); 467 468 if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { 469 return slidingChild; 470 } 471 } 472 return null; 473 } 474 475 public boolean canChildBeExpanded(View v) { 476 return v instanceof ExpandableNotificationRow 477 && ((ExpandableNotificationRow) v).isExpandable(); 478 } 479 480 public void setUserExpandedChild(View v, boolean userExpanded) { 481 if (v instanceof ExpandableNotificationRow) { 482 ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); 483 } 484 } 485 486 public void setUserLockedChild(View v, boolean userLocked) { 487 if (v instanceof ExpandableNotificationRow) { 488 ((ExpandableNotificationRow) v).setUserLocked(userLocked); 489 } 490 } 491 492 public View getChildContentView(View v) { 493 return v; 494 } 495 496 public boolean canChildBeDismissed(View v) { 497 final View veto = v.findViewById(R.id.veto); 498 return (veto != null && veto.getVisibility() != View.GONE); 499 } 500 501 private void setSwipingInProgress(boolean isSwiped) { 502 mSwipingInProgress = isSwiped; 503 if(isSwiped) { 504 requestDisallowInterceptTouchEvent(true); 505 } 506 } 507 508 @Override 509 protected void onConfigurationChanged(Configuration newConfig) { 510 super.onConfigurationChanged(newConfig); 511 float densityScale = getResources().getDisplayMetrics().density; 512 mSwipeHelper.setDensityScale(densityScale); 513 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 514 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 515 initView(getContext()); 516 } 517 518 public void dismissRowAnimated(View child, int vel) { 519 mSwipeHelper.dismissChild(child, vel); 520 } 521 522 @Override 523 public boolean onTouchEvent(MotionEvent ev) { 524 if (!isEnabled()) { 525 return false; 526 } 527 boolean scrollerWantsIt = false; 528 if (!mSwipingInProgress) { 529 scrollerWantsIt = onScrollTouch(ev); 530 } 531 boolean horizontalSwipeWantsIt = false; 532 if (!mIsBeingDragged) { 533 horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); 534 } 535 return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev); 536 } 537 538 private boolean onScrollTouch(MotionEvent ev) { 539 initVelocityTrackerIfNotExists(); 540 mVelocityTracker.addMovement(ev); 541 542 final int action = ev.getAction(); 543 544 switch (action & MotionEvent.ACTION_MASK) { 545 case MotionEvent.ACTION_DOWN: { 546 if (getChildCount() == 0 || !isInContentBounds(ev)) { 547 return false; 548 } 549 boolean isBeingDragged = !mScroller.isFinished(); 550 setIsBeingDragged(isBeingDragged); 551 552 /* 553 * If being flinged and user touches, stop the fling. isFinished 554 * will be false if being flinged. 555 */ 556 if (!mScroller.isFinished()) { 557 mScroller.forceFinished(true); 558 } 559 560 // Remember where the motion event started 561 mLastMotionY = (int) ev.getY(); 562 mActivePointerId = ev.getPointerId(0); 563 break; 564 } 565 case MotionEvent.ACTION_MOVE: 566 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 567 if (activePointerIndex == -1) { 568 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 569 break; 570 } 571 572 final int y = (int) ev.getY(activePointerIndex); 573 int deltaY = mLastMotionY - y; 574 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 575 setIsBeingDragged(true); 576 if (deltaY > 0) { 577 deltaY -= mTouchSlop; 578 } else { 579 deltaY += mTouchSlop; 580 } 581 } 582 if (mIsBeingDragged) { 583 // Scroll to follow the motion event 584 mLastMotionY = y; 585 final int range = getScrollRange(); 586 587 float scrollAmount; 588 if (deltaY < 0) { 589 scrollAmount = overScrollDown(deltaY); 590 } else { 591 scrollAmount = overScrollUp(deltaY, range); 592 } 593 594 // Calling overScrollBy will call onOverScrolled, which 595 // calls onScrollChanged if applicable. 596 if (scrollAmount != 0.0f) { 597 // The scrolling motion could not be compensated with the 598 // existing overScroll, we have to scroll the view 599 overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY, 600 0, range, 0, getHeight() / 2, true); 601 } 602 } 603 break; 604 case MotionEvent.ACTION_UP: 605 if (mIsBeingDragged) { 606 final VelocityTracker velocityTracker = mVelocityTracker; 607 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 608 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 609 610 if (getChildCount() > 0) { 611 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 612 fling(-initialVelocity); 613 } else { 614 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, 615 getScrollRange())) { 616 postInvalidateOnAnimation(); 617 } 618 } 619 } 620 621 mActivePointerId = INVALID_POINTER; 622 endDrag(); 623 } 624 break; 625 case MotionEvent.ACTION_CANCEL: 626 if (mIsBeingDragged && getChildCount() > 0) { 627 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 628 postInvalidateOnAnimation(); 629 } 630 mActivePointerId = INVALID_POINTER; 631 endDrag(); 632 } 633 break; 634 case MotionEvent.ACTION_POINTER_DOWN: { 635 final int index = ev.getActionIndex(); 636 mLastMotionY = (int) ev.getY(index); 637 mActivePointerId = ev.getPointerId(index); 638 break; 639 } 640 case MotionEvent.ACTION_POINTER_UP: 641 onSecondaryPointerUp(ev); 642 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 643 break; 644 } 645 return true; 646 } 647 648 /** 649 * Perform a scroll upwards and adapt the overscroll amounts accordingly 650 * 651 * @param deltaY The amount to scroll upwards, has to be positive. 652 * @return The amount of scrolling to be performed by the scroller, 653 * not handled by the overScroll amount. 654 */ 655 private float overScrollUp(int deltaY, int range) { 656 deltaY = Math.max(deltaY, 0); 657 float currentTopAmount = getCurrentOverScrollAmount(true); 658 float newTopAmount = currentTopAmount - deltaY; 659 if (currentTopAmount > 0) { 660 setOverScrollAmount(newTopAmount, true /* onTop */, 661 false /* animate */); 662 } 663 // Top overScroll might not grab all scrolling motion, 664 // we have to scroll as well. 665 float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f; 666 float newScrollY = mOwnScrollY + scrollAmount; 667 if (newScrollY > range) { 668 float currentBottomPixels = getCurrentOverScrolledPixels(false); 669 // We overScroll on the top 670 setOverScrolledPixels(currentBottomPixels + newScrollY - range, 671 false /* onTop */, 672 false /* animate */); 673 mOwnScrollY = range; 674 scrollAmount = 0.0f; 675 } 676 return scrollAmount; 677 } 678 679 /** 680 * Perform a scroll downward and adapt the overscroll amounts accordingly 681 * 682 * @param deltaY The amount to scroll downwards, has to be negative. 683 * @return The amount of scrolling to be performed by the scroller, 684 * not handled by the overScroll amount. 685 */ 686 private float overScrollDown(int deltaY) { 687 deltaY = Math.min(deltaY, 0); 688 float currentBottomAmount = getCurrentOverScrollAmount(false); 689 float newBottomAmount = currentBottomAmount + deltaY; 690 if (currentBottomAmount > 0) { 691 setOverScrollAmount(newBottomAmount, false /* onTop */, 692 false /* animate */); 693 } 694 // Bottom overScroll might not grab all scrolling motion, 695 // we have to scroll as well. 696 float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f; 697 float newScrollY = mOwnScrollY + scrollAmount; 698 if (newScrollY < 0) { 699 float currentTopPixels = getCurrentOverScrolledPixels(true); 700 // We overScroll on the top 701 setOverScrolledPixels(currentTopPixels - newScrollY, 702 true /* onTop */, 703 false /* animate */); 704 mOwnScrollY = 0; 705 scrollAmount = 0.0f; 706 } 707 return scrollAmount; 708 } 709 710 private void onSecondaryPointerUp(MotionEvent ev) { 711 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 712 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 713 final int pointerId = ev.getPointerId(pointerIndex); 714 if (pointerId == mActivePointerId) { 715 // This was our active pointer going up. Choose a new 716 // active pointer and adjust accordingly. 717 // TODO: Make this decision more intelligent. 718 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 719 mLastMotionY = (int) ev.getY(newPointerIndex); 720 mActivePointerId = ev.getPointerId(newPointerIndex); 721 if (mVelocityTracker != null) { 722 mVelocityTracker.clear(); 723 } 724 } 725 } 726 727 private void initVelocityTrackerIfNotExists() { 728 if (mVelocityTracker == null) { 729 mVelocityTracker = VelocityTracker.obtain(); 730 } 731 } 732 733 private void recycleVelocityTracker() { 734 if (mVelocityTracker != null) { 735 mVelocityTracker.recycle(); 736 mVelocityTracker = null; 737 } 738 } 739 740 private void initOrResetVelocityTracker() { 741 if (mVelocityTracker == null) { 742 mVelocityTracker = VelocityTracker.obtain(); 743 } else { 744 mVelocityTracker.clear(); 745 } 746 } 747 748 @Override 749 public void computeScroll() { 750 if (mScroller.computeScrollOffset()) { 751 // This is called at drawing time by ViewGroup. 752 int oldX = mScrollX; 753 int oldY = mOwnScrollY; 754 int x = mScroller.getCurrX(); 755 int y = mScroller.getCurrY(); 756 757 if (oldX != x || oldY != y) { 758 final int range = getScrollRange(); 759 if (y < 0 && oldY >= 0 || y > range && oldY <= range) { 760 float currVelocity = mScroller.getCurrVelocity(); 761 if (currVelocity >= mMinimumVelocity * 20) { 762 mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance; 763 } 764 } 765 766 overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 767 0, (int) (mMaxOverScroll), false); 768 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 769 } 770 771 // Keep on drawing until the animation has finished. 772 postInvalidateOnAnimation(); 773 } 774 } 775 776 @Override 777 protected int computeVerticalScrollRange() { 778 // needed for the overScroller 779 return mContentHeight; 780 } 781 782 /** 783 * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded 784 * overscroll effect based on numPixels. By default this will also cancel animations on the 785 * same overScroll edge. 786 * 787 * @param numPixels The amount of pixels to overScroll by. These will be scaled according to 788 * the rubber-banding logic. 789 * @param onTop Should the effect be applied on top of the scroller. 790 * @param animate Should an animation be performed. 791 */ 792 public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) { 793 setOverScrollAmount(numPixels * RUBBER_BAND_FACTOR, onTop, animate, true); 794 } 795 796 /** 797 * Set the effective overScroll amount which will be directly reflected in the layout. 798 * By default this will also cancel animations on the same overScroll edge. 799 * 800 * @param amount The amount to overScroll by. 801 * @param onTop Should the effect be applied on top of the scroller. 802 * @param animate Should an animation be performed. 803 */ 804 public void setOverScrollAmount(float amount, boolean onTop, boolean animate) { 805 setOverScrollAmount(amount, onTop, animate, true); 806 } 807 808 /** 809 * Set the effective overScroll amount which will be directly reflected in the layout. 810 * 811 * @param amount The amount to overScroll by. 812 * @param onTop Should the effect be applied on top of the scroller. 813 * @param animate Should an animation be performed. 814 * @param cancelAnimators Should running animations be cancelled. 815 */ 816 public void setOverScrollAmount(float amount, boolean onTop, boolean animate, 817 boolean cancelAnimators) { 818 if (cancelAnimators) { 819 mStateAnimator.cancelOverScrollAnimators(onTop); 820 } 821 setOverScrollAmountInternal(amount, onTop, animate); 822 } 823 824 private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate) { 825 amount = Math.max(0, amount); 826 if (animate) { 827 mStateAnimator.animateOverScrollToAmount(amount, onTop); 828 } else { 829 setOverScrolledPixels(amount / RUBBER_BAND_FACTOR, onTop); 830 mAmbientState.setOverScrollAmount(amount, onTop); 831 requestChildrenUpdate(); 832 } 833 } 834 835 public float getCurrentOverScrollAmount(boolean top) { 836 return mAmbientState.getOverScrollAmount(top); 837 } 838 839 public float getCurrentOverScrolledPixels(boolean top) { 840 return top? mOverScrolledTopPixels : mOverScrolledBottomPixels; 841 } 842 843 private void setOverScrolledPixels(float amount, boolean onTop) { 844 if (onTop) { 845 mOverScrolledTopPixels = amount; 846 } else { 847 mOverScrolledBottomPixels = amount; 848 } 849 } 850 851 private void customScrollTo(int y) { 852 mOwnScrollY = y; 853 updateChildren(); 854 } 855 856 @Override 857 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 858 // Treat animating scrolls differently; see #computeScroll() for why. 859 if (!mScroller.isFinished()) { 860 final int oldX = mScrollX; 861 final int oldY = mOwnScrollY; 862 mScrollX = scrollX; 863 mOwnScrollY = scrollY; 864 if (clampedY) { 865 springBack(); 866 } else { 867 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 868 invalidateParentIfNeeded(); 869 updateChildren(); 870 } 871 } else { 872 customScrollTo(scrollY); 873 scrollTo(scrollX, mScrollY); 874 } 875 } 876 877 private void springBack() { 878 int scrollRange = getScrollRange(); 879 boolean overScrolledTop = mOwnScrollY <= 0; 880 boolean overScrolledBottom = mOwnScrollY >= scrollRange; 881 if (overScrolledTop || overScrolledBottom) { 882 boolean onTop; 883 float newAmount; 884 if (overScrolledTop) { 885 onTop = true; 886 newAmount = -mOwnScrollY; 887 mOwnScrollY = 0; 888 } else { 889 onTop = false; 890 newAmount = mOwnScrollY - scrollRange; 891 mOwnScrollY = scrollRange; 892 } 893 setOverScrollAmount(newAmount, onTop, false); 894 setOverScrollAmount(0.0f, onTop, true); 895 mScroller.forceFinished(true); 896 } 897 } 898 899 private int getScrollRange() { 900 int scrollRange = 0; 901 ExpandableView firstChild = (ExpandableView) getFirstChildNotGone(); 902 if (firstChild != null) { 903 int contentHeight = getContentHeight(); 904 int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild); 905 scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize 906 + mBottomStackSlowDownHeight); 907 if (scrollRange > 0) { 908 View lastChild = getLastChildNotGone(); 909 // We want to at least be able collapse the first item and not ending in a weird 910 // end state. 911 scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize); 912 } 913 } 914 return scrollRange; 915 } 916 917 /** 918 * @return the first child which has visibility unequal to GONE 919 */ 920 private View getFirstChildNotGone() { 921 int childCount = getChildCount(); 922 for (int i = 0; i < childCount; i++) { 923 View child = getChildAt(i); 924 if (child.getVisibility() != View.GONE) { 925 return child; 926 } 927 } 928 return null; 929 } 930 931 /** 932 * @return the last child which has visibility unequal to GONE 933 */ 934 private View getLastChildNotGone() { 935 int childCount = getChildCount(); 936 for (int i = childCount - 1; i >= 0; i--) { 937 View child = getChildAt(i); 938 if (child.getVisibility() != View.GONE) { 939 return child; 940 } 941 } 942 return null; 943 } 944 945 private int getMaxExpandHeight(View view) { 946 if (view instanceof ExpandableNotificationRow) { 947 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 948 return row.getIntrinsicHeight(); 949 } 950 return view.getHeight(); 951 } 952 953 private int getContentHeight() { 954 return mContentHeight; 955 } 956 957 private void updateContentHeight() { 958 int height = 0; 959 for (int i = 0; i < getChildCount(); i++) { 960 View child = getChildAt(i); 961 if (child.getVisibility() != View.GONE) { 962 if (height != 0) { 963 // add the padding before this element 964 height += mPaddingBetweenElements; 965 } 966 if (child instanceof ExpandableNotificationRow) { 967 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 968 height += row.getIntrinsicHeight(); 969 } else if (child instanceof ExpandableView) { 970 ExpandableView expandableView = (ExpandableView) child; 971 height += expandableView.getActualHeight(); 972 } 973 } 974 } 975 mContentHeight = height + mTopPadding; 976 } 977 978 /** 979 * Fling the scroll view 980 * 981 * @param velocityY The initial velocity in the Y direction. Positive 982 * numbers mean that the finger/cursor is moving down the screen, 983 * which means we want to scroll towards the top. 984 */ 985 private void fling(int velocityY) { 986 if (getChildCount() > 0) { 987 int height = (int) getLayoutHeight(); 988 int bottom = getContentHeight(); 989 990 float topAmount = getCurrentOverScrollAmount(true); 991 float bottomAmount = getCurrentOverScrollAmount(false); 992 if (velocityY < 0 && topAmount > 0) { 993 mOwnScrollY -= (int) topAmount; 994 setOverScrollAmount(0, true, false); 995 mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR 996 * mOverflingDistance + topAmount; 997 } else if (velocityY > 0 && bottomAmount > 0) { 998 mOwnScrollY += bottomAmount; 999 setOverScrollAmount(0, false, false); 1000 mMaxOverScroll = Math.abs(velocityY) / 1000f * RUBBER_BAND_FACTOR 1001 * mOverflingDistance + bottomAmount; 1002 } else { 1003 // it will be set once we reach the boundary 1004 mMaxOverScroll = 0.0f; 1005 } 1006 mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0, 1007 Math.max(0, bottom - height), 0, height/2); 1008 1009 postInvalidateOnAnimation(); 1010 } 1011 } 1012 1013 private void endDrag() { 1014 setIsBeingDragged(false); 1015 1016 recycleVelocityTracker(); 1017 1018 if (getCurrentOverScrollAmount(true /* onTop */) > 0) { 1019 setOverScrollAmount(0, true /* onTop */, true /* animate */); 1020 } 1021 if (getCurrentOverScrollAmount(false /* onTop */) > 0) { 1022 setOverScrollAmount(0, false /* onTop */, true /* animate */); 1023 } 1024 } 1025 1026 @Override 1027 public boolean onInterceptTouchEvent(MotionEvent ev) { 1028 boolean scrollWantsIt = false; 1029 if (!mSwipingInProgress) { 1030 scrollWantsIt = onInterceptTouchEventScroll(ev); 1031 } 1032 boolean swipeWantsIt = false; 1033 if (!mIsBeingDragged) { 1034 swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); 1035 } 1036 return swipeWantsIt || scrollWantsIt || 1037 super.onInterceptTouchEvent(ev); 1038 } 1039 1040 @Override 1041 protected void onViewRemoved(View child) { 1042 super.onViewRemoved(child); 1043 ((ExpandableView) child).setOnHeightChangedListener(null); 1044 mCurrentStackScrollState.removeViewStateForView(child); 1045 mStackScrollAlgorithm.notifyChildrenChanged(this); 1046 updateScrollStateForRemovedChild(child); 1047 if (mIsExpanded) { 1048 1049 if (!mChildrenToAddAnimated.contains(child)) { 1050 // Generate Animations 1051 mChildrenToRemoveAnimated.add(child); 1052 mNeedsAnimation = true; 1053 } else { 1054 mChildrenToAddAnimated.remove(child); 1055 } 1056 } 1057 } 1058 1059 /** 1060 * Updates the scroll position when a child was removed 1061 * 1062 * @param removedChild the removed child 1063 */ 1064 private void updateScrollStateForRemovedChild(View removedChild) { 1065 int startingPosition = getPositionInLinearLayout(removedChild); 1066 int childHeight = removedChild.getHeight() + mPaddingBetweenElements; 1067 int endPosition = startingPosition + childHeight; 1068 if (endPosition <= mOwnScrollY) { 1069 // This child is fully scrolled of the top, so we have to deduct its height from the 1070 // scrollPosition 1071 mOwnScrollY -= childHeight; 1072 } else if (startingPosition < mOwnScrollY) { 1073 // This child is currently being scrolled into, set the scroll position to the start of 1074 // this child 1075 mOwnScrollY = startingPosition; 1076 } 1077 } 1078 1079 private int getPositionInLinearLayout(View requestedChild) { 1080 int position = 0; 1081 for (int i = 0; i < getChildCount(); i++) { 1082 View child = getChildAt(i); 1083 if (child == requestedChild) { 1084 return position; 1085 } 1086 if (child.getVisibility() != View.GONE) { 1087 position += child.getHeight(); 1088 if (i < getChildCount()-1) { 1089 position += mPaddingBetweenElements; 1090 } 1091 } 1092 } 1093 return 0; 1094 } 1095 1096 @Override 1097 protected void onViewAdded(View child) { 1098 super.onViewAdded(child); 1099 mStackScrollAlgorithm.notifyChildrenChanged(this); 1100 ((ExpandableView) child).setOnHeightChangedListener(this); 1101 if (child.getVisibility() != View.GONE) { 1102 generateAddAnimation(child); 1103 } 1104 } 1105 1106 public void generateAddAnimation(View child) { 1107 if (mIsExpanded) { 1108 1109 // Generate Animations 1110 mChildrenToAddAnimated.add(child); 1111 mNeedsAnimation = true; 1112 } 1113 } 1114 1115 /** 1116 * Change the position of child to a new location 1117 * 1118 * @param child the view to change the position for 1119 * @param newIndex the new index 1120 */ 1121 public void changeViewPosition(View child, int newIndex) { 1122 if (child != null && child.getParent() == this) { 1123 // TODO: handle this 1124 } 1125 } 1126 1127 private void startAnimationToState() { 1128 if (mNeedsAnimation) { 1129 generateChildHierarchyEvents(); 1130 mNeedsAnimation = false; 1131 } 1132 if (!mAnimationEvents.isEmpty()) { 1133 mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState); 1134 } else { 1135 applyCurrentState(); 1136 } 1137 } 1138 1139 private void generateChildHierarchyEvents() { 1140 generateChildAdditionEvents(); 1141 generateChildRemovalEvents(); 1142 generateSnapBackEvents(); 1143 generateDragEvents(); 1144 generateTopPaddingEvent(); 1145 generateActivateEvent(); 1146 generateDimmedEvent(); 1147 mNeedsAnimation = false; 1148 } 1149 1150 private void generateSnapBackEvents() { 1151 for (View child : mSnappedBackChildren) { 1152 mAnimationEvents.add(new AnimationEvent(child, 1153 AnimationEvent.ANIMATION_TYPE_SNAP_BACK)); 1154 } 1155 mSnappedBackChildren.clear(); 1156 } 1157 1158 private void generateDragEvents() { 1159 for (View child : mDragAnimPendingChildren) { 1160 mAnimationEvents.add(new AnimationEvent(child, 1161 AnimationEvent.ANIMATION_TYPE_START_DRAG)); 1162 } 1163 mDragAnimPendingChildren.clear(); 1164 } 1165 1166 private void generateChildRemovalEvents() { 1167 for (View child : mChildrenToRemoveAnimated) { 1168 boolean childWasSwipedOut = mSwipedOutViews.contains(child); 1169 int animationType = childWasSwipedOut 1170 ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT 1171 : AnimationEvent.ANIMATION_TYPE_REMOVE; 1172 mAnimationEvents.add(new AnimationEvent(child, animationType)); 1173 } 1174 mSwipedOutViews.clear(); 1175 mChildrenToRemoveAnimated.clear(); 1176 } 1177 1178 private void generateChildAdditionEvents() { 1179 for (View child : mChildrenToAddAnimated) { 1180 mAnimationEvents.add(new AnimationEvent(child, 1181 AnimationEvent.ANIMATION_TYPE_ADD)); 1182 } 1183 mChildrenToAddAnimated.clear(); 1184 } 1185 1186 private void generateTopPaddingEvent() { 1187 if (mTopPaddingNeedsAnimation) { 1188 mAnimationEvents.add( 1189 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED)); 1190 } 1191 mTopPaddingNeedsAnimation = false; 1192 } 1193 1194 private void generateActivateEvent() { 1195 if (mActivateNeedsAnimation) { 1196 mAnimationEvents.add( 1197 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD)); 1198 } 1199 mActivateNeedsAnimation = false; 1200 } 1201 1202 private void generateDimmedEvent() { 1203 if (mDimmedNeedsAnimation) { 1204 mAnimationEvents.add( 1205 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED)); 1206 } 1207 mDimmedNeedsAnimation = false; 1208 } 1209 1210 private boolean onInterceptTouchEventScroll(MotionEvent ev) { 1211 /* 1212 * This method JUST determines whether we want to intercept the motion. 1213 * If we return true, onMotionEvent will be called and we do the actual 1214 * scrolling there. 1215 */ 1216 1217 /* 1218 * Shortcut the most recurring case: the user is in the dragging 1219 * state and he is moving his finger. We want to intercept this 1220 * motion. 1221 */ 1222 final int action = ev.getAction(); 1223 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 1224 return true; 1225 } 1226 1227 /* 1228 * Don't try to intercept touch if we can't scroll anyway. 1229 */ 1230 if (mOwnScrollY == 0 && getScrollRange() == 0) { 1231 return false; 1232 } 1233 1234 switch (action & MotionEvent.ACTION_MASK) { 1235 case MotionEvent.ACTION_MOVE: { 1236 /* 1237 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1238 * whether the user has moved far enough from his original down touch. 1239 */ 1240 1241 /* 1242 * Locally do absolute value. mLastMotionY is set to the y value 1243 * of the down event. 1244 */ 1245 final int activePointerId = mActivePointerId; 1246 if (activePointerId == INVALID_POINTER) { 1247 // If we don't have a valid id, the touch down wasn't on content. 1248 break; 1249 } 1250 1251 final int pointerIndex = ev.findPointerIndex(activePointerId); 1252 if (pointerIndex == -1) { 1253 Log.e(TAG, "Invalid pointerId=" + activePointerId 1254 + " in onInterceptTouchEvent"); 1255 break; 1256 } 1257 1258 final int y = (int) ev.getY(pointerIndex); 1259 final int yDiff = Math.abs(y - mLastMotionY); 1260 if (yDiff > mTouchSlop) { 1261 setIsBeingDragged(true); 1262 mLastMotionY = y; 1263 initVelocityTrackerIfNotExists(); 1264 mVelocityTracker.addMovement(ev); 1265 } 1266 break; 1267 } 1268 1269 case MotionEvent.ACTION_DOWN: { 1270 final int y = (int) ev.getY(); 1271 if (getChildAtPosition(ev.getX(), y) == null) { 1272 setIsBeingDragged(false); 1273 recycleVelocityTracker(); 1274 break; 1275 } 1276 1277 /* 1278 * Remember location of down touch. 1279 * ACTION_DOWN always refers to pointer index 0. 1280 */ 1281 mLastMotionY = y; 1282 mActivePointerId = ev.getPointerId(0); 1283 1284 initOrResetVelocityTracker(); 1285 mVelocityTracker.addMovement(ev); 1286 /* 1287 * If being flinged and user touches the screen, initiate drag; 1288 * otherwise don't. mScroller.isFinished should be false when 1289 * being flinged. 1290 */ 1291 boolean isBeingDragged = !mScroller.isFinished(); 1292 setIsBeingDragged(isBeingDragged); 1293 break; 1294 } 1295 1296 case MotionEvent.ACTION_CANCEL: 1297 case MotionEvent.ACTION_UP: 1298 /* Release the drag */ 1299 setIsBeingDragged(false); 1300 mActivePointerId = INVALID_POINTER; 1301 recycleVelocityTracker(); 1302 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 1303 postInvalidateOnAnimation(); 1304 } 1305 break; 1306 case MotionEvent.ACTION_POINTER_UP: 1307 onSecondaryPointerUp(ev); 1308 break; 1309 } 1310 1311 /* 1312 * The only time we want to intercept motion events is if we are in the 1313 * drag mode. 1314 */ 1315 return mIsBeingDragged; 1316 } 1317 1318 /** 1319 * @return Whether the specified motion event is actually happening over the content. 1320 */ 1321 private boolean isInContentBounds(MotionEvent event) { 1322 return event.getY() < getHeight() - getEmptyBottomMargin(); 1323 } 1324 1325 private void setIsBeingDragged(boolean isDragged) { 1326 mIsBeingDragged = isDragged; 1327 if (isDragged) { 1328 requestDisallowInterceptTouchEvent(true); 1329 mSwipeHelper.removeLongPressCallback(); 1330 } 1331 } 1332 1333 @Override 1334 public void onWindowFocusChanged(boolean hasWindowFocus) { 1335 super.onWindowFocusChanged(hasWindowFocus); 1336 if (!hasWindowFocus) { 1337 mSwipeHelper.removeLongPressCallback(); 1338 } 1339 } 1340 1341 @Override 1342 public boolean isScrolledToTop() { 1343 return mOwnScrollY == 0; 1344 } 1345 1346 @Override 1347 public boolean isScrolledToBottom() { 1348 return mOwnScrollY >= getScrollRange(); 1349 } 1350 1351 @Override 1352 public View getHostView() { 1353 return this; 1354 } 1355 1356 public int getEmptyBottomMargin() { 1357 int emptyMargin = mMaxLayoutHeight - mContentHeight; 1358 if (needsHeightAdaption()) { 1359 emptyMargin = emptyMargin - mBottomStackSlowDownHeight - mBottomStackPeekSize; 1360 } 1361 return Math.max(emptyMargin, 0); 1362 } 1363 1364 public void onExpansionStarted() { 1365 mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); 1366 } 1367 1368 public void onExpansionStopped() { 1369 mStackScrollAlgorithm.onExpansionStopped(); 1370 } 1371 1372 private void setIsExpanded(boolean isExpanded) { 1373 mIsExpanded = isExpanded; 1374 mStackScrollAlgorithm.setIsExpanded(isExpanded); 1375 if (!isExpanded) { 1376 mOwnScrollY = 0; 1377 } 1378 } 1379 1380 @Override 1381 public void onHeightChanged(ExpandableView view) { 1382 updateContentHeight(); 1383 updateScrollPositionIfNecessary(); 1384 if (mOnHeightChangedListener != null) { 1385 mOnHeightChangedListener.onHeightChanged(view); 1386 } 1387 requestChildrenUpdate(); 1388 } 1389 1390 public void setOnHeightChangedListener( 1391 ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { 1392 this.mOnHeightChangedListener = mOnHeightChangedListener; 1393 } 1394 1395 public void onChildAnimationFinished() { 1396 requestChildrenUpdate(); 1397 mAnimationEvents.clear(); 1398 } 1399 1400 /** 1401 * See {@link AmbientState#setDimmed}. 1402 */ 1403 public void setDimmed(boolean dimmed, boolean animate) { 1404 mStackScrollAlgorithm.setDimmed(dimmed); 1405 mAmbientState.setDimmed(dimmed); 1406 updatePadding(dimmed); 1407 if (animate) { 1408 mDimmedNeedsAnimation = true; 1409 mNeedsAnimation = true; 1410 } 1411 requestChildrenUpdate(); 1412 } 1413 1414 /** 1415 * See {@link AmbientState#setActivatedChild}. 1416 */ 1417 public void setActivatedChild(View activatedChild) { 1418 mAmbientState.setActivatedChild(activatedChild); 1419 mActivateNeedsAnimation = true; 1420 mNeedsAnimation = true; 1421 requestChildrenUpdate(); 1422 } 1423 1424 public View getActivatedChild() { 1425 return mAmbientState.getActivatedChild(); 1426 } 1427 1428 private void applyCurrentState() { 1429 mCurrentStackScrollState.apply(); 1430 if (mListener != null) { 1431 mListener.onChildLocationsChanged(this); 1432 } 1433 } 1434 1435 /** 1436 * A listener that is notified when some child locations might have changed. 1437 */ 1438 public interface OnChildLocationsChangedListener { 1439 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); 1440 } 1441 1442 static class AnimationEvent { 1443 1444 static AnimationFilter[] FILTERS = new AnimationFilter[] { 1445 1446 // ANIMATION_TYPE_ADD 1447 new AnimationFilter() 1448 .animateAlpha() 1449 .animateHeight() 1450 .animateY() 1451 .animateZ(), 1452 1453 // ANIMATION_TYPE_REMOVE 1454 new AnimationFilter() 1455 .animateAlpha() 1456 .animateHeight() 1457 .animateY() 1458 .animateZ(), 1459 1460 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 1461 new AnimationFilter() 1462 .animateAlpha() 1463 .animateHeight() 1464 .animateY() 1465 .animateZ(), 1466 1467 // ANIMATION_TYPE_TOP_PADDING_CHANGED 1468 new AnimationFilter() 1469 .animateAlpha() 1470 .animateHeight() 1471 .animateY() 1472 .animateDimmed() 1473 .animateScale() 1474 .animateZ(), 1475 1476 // ANIMATION_TYPE_START_DRAG 1477 new AnimationFilter() 1478 .animateAlpha(), 1479 1480 // ANIMATION_TYPE_SNAP_BACK 1481 new AnimationFilter() 1482 .animateAlpha(), 1483 1484 // ANIMATION_TYPE_ACTIVATED_CHILD 1485 new AnimationFilter() 1486 .animateScale() 1487 .animateAlpha(), 1488 1489 // ANIMATION_TYPE_DIMMED 1490 new AnimationFilter() 1491 .animateY() 1492 .animateScale() 1493 .animateDimmed() 1494 }; 1495 1496 static int[] LENGTHS = new int[] { 1497 1498 // ANIMATION_TYPE_ADD 1499 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1500 1501 // ANIMATION_TYPE_REMOVE 1502 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1503 1504 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 1505 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1506 1507 // ANIMATION_TYPE_TOP_PADDING_CHANGED 1508 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1509 1510 // ANIMATION_TYPE_START_DRAG 1511 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1512 1513 // ANIMATION_TYPE_SNAP_BACK 1514 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1515 1516 // ANIMATION_TYPE_ACTIVATED_CHILD 1517 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 1518 1519 // ANIMATION_TYPE_DIMMED 1520 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 1521 }; 1522 1523 static int ANIMATION_TYPE_ADD = 0; 1524 static int ANIMATION_TYPE_REMOVE = 1; 1525 static int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2; 1526 static int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3; 1527 static int ANIMATION_TYPE_START_DRAG = 4; 1528 static int ANIMATION_TYPE_SNAP_BACK = 5; 1529 static int ANIMATION_TYPE_ACTIVATED_CHILD = 6; 1530 static int ANIMATION_TYPE_DIMMED = 7; 1531 1532 final long eventStartTime; 1533 final View changingView; 1534 final int animationType; 1535 final AnimationFilter filter; 1536 final long length; 1537 1538 AnimationEvent(View view, int type) { 1539 eventStartTime = AnimationUtils.currentAnimationTimeMillis(); 1540 changingView = view; 1541 animationType = type; 1542 filter = FILTERS[type]; 1543 length = LENGTHS[type]; 1544 } 1545 1546 /** 1547 * Combines the length of several animation events into a single value. 1548 * 1549 * @param events The events of the lengths to combine. 1550 * @return The combined length. This is just the maximum of all length. 1551 */ 1552 static long combineLength(ArrayList<AnimationEvent> events) { 1553 long length = 0; 1554 int size = events.size(); 1555 for (int i = 0; i < size; i++) { 1556 length = Math.max(length, events.get(i).length); 1557 } 1558 return length; 1559 } 1560 } 1561 1562} 1563