NotificationStackScrollLayout.java revision 1408eb5a58d669933c701e347fd3498ceab70f3c
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; 21import android.graphics.Canvas; 22import android.graphics.Paint; 23import android.util.AttributeSet; 24import android.util.Log; 25import android.view.MotionEvent; 26import android.view.VelocityTracker; 27import android.view.View; 28import android.view.ViewConfiguration; 29import android.view.ViewGroup; 30import android.view.ViewTreeObserver; 31import android.view.animation.AnimationUtils; 32import android.widget.OverScroller; 33import com.android.systemui.ExpandHelper; 34import com.android.systemui.R; 35import com.android.systemui.SwipeHelper; 36import com.android.systemui.statusbar.ExpandableNotificationRow; 37import com.android.systemui.statusbar.ExpandableView; 38import com.android.systemui.statusbar.SpeedBumpView; 39import com.android.systemui.statusbar.policy.ScrollAdapter; 40import com.android.systemui.statusbar.stack.StackScrollState.ViewState; 41 42import java.util.ArrayList; 43 44/** 45 * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack. 46 */ 47public class NotificationStackScrollLayout extends ViewGroup 48 implements SwipeHelper.Callback, ExpandHelper.Callback, ScrollAdapter, 49 ExpandableView.OnHeightChangedListener { 50 51 private static final String TAG = "NotificationStackScrollLayout"; 52 private static final boolean DEBUG = false; 53 private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f; 54 private static final float RUBBER_BAND_FACTOR_AFTER_EXPAND = 0.15f; 55 56 /** 57 * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}. 58 */ 59 private static final int INVALID_POINTER = -1; 60 61 private ExpandHelper mExpandHelper; 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 mOverflingDistance; 74 private float mMaxOverScroll; 75 private boolean mIsBeingDragged; 76 private int mLastMotionY; 77 private int mDownX; 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 mPaddingBetweenElements; 87 private int mPaddingBetweenElementsDimmed; 88 private int mPaddingBetweenElementsNormal; 89 private int mTopPadding; 90 91 /** 92 * The algorithm which calculates the properties for our children 93 */ 94 private StackScrollAlgorithm mStackScrollAlgorithm; 95 96 /** 97 * The current State this Layout is in 98 */ 99 private StackScrollState mCurrentStackScrollState = new StackScrollState(this); 100 private AmbientState mAmbientState = new AmbientState(); 101 private ArrayList<View> mChildrenToAddAnimated = new ArrayList<View>(); 102 private ArrayList<View> mChildrenToRemoveAnimated = new ArrayList<View>(); 103 private ArrayList<View> mSnappedBackChildren = new ArrayList<View>(); 104 private ArrayList<View> mDragAnimPendingChildren = new ArrayList<View>(); 105 private ArrayList<View> mChildrenChangingPositions = 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 private boolean mAnimationsEnabled; 111 112 /** 113 * The raw amount of the overScroll on the top, which is not rubber-banded. 114 */ 115 private float mOverScrolledTopPixels; 116 117 /** 118 * The raw amount of the overScroll on the bottom, which is not rubber-banded. 119 */ 120 private float mOverScrolledBottomPixels; 121 122 private OnChildLocationsChangedListener mListener; 123 private OnOverscrollTopChangedListener mOverscrollTopChangedListener; 124 private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; 125 private boolean mNeedsAnimation; 126 private boolean mTopPaddingNeedsAnimation; 127 private boolean mDimmedNeedsAnimation; 128 private boolean mActivateNeedsAnimation; 129 private boolean mIsExpanded = true; 130 private boolean mChildrenUpdateRequested; 131 private SpeedBumpView mSpeedBumpView; 132 private boolean mIsExpansionChanging; 133 private boolean mExpandingNotification; 134 private boolean mExpandedInThisMotion; 135 private boolean mScrollingEnabled; 136 137 /** 138 * Was the scroller scrolled to the top when the down motion was observed? 139 */ 140 private boolean mScrolledToTopOnFirstDown; 141 142 /** 143 * The minimal amount of over scroll which is needed in order to switch to the quick settings 144 * when over scrolling on a expanded card. 145 */ 146 private float mMinTopOverScrollToEscape; 147 private int mIntrinsicPadding; 148 private int mNotificationTopPadding; 149 private int mMinStackHeight; 150 private boolean mDontReportNextOverScroll; 151 152 /** 153 * The maximum scrollPosition which we are allowed to reach when a notification was expanded. 154 * This is needed to avoid scrolling too far after the notification was collapsed in the same 155 * motion. 156 */ 157 private int mMaxScrollAfterExpand; 158 159 /** 160 * Should in this touch motion only be scrolling allowed? It's true when the scroller was 161 * animating. 162 */ 163 private boolean mOnlyScrollingInThisMotion; 164 165 private ViewTreeObserver.OnPreDrawListener mChildrenUpdater 166 = new ViewTreeObserver.OnPreDrawListener() { 167 @Override 168 public boolean onPreDraw() { 169 updateChildren(); 170 mChildrenUpdateRequested = false; 171 getViewTreeObserver().removeOnPreDrawListener(this); 172 return true; 173 } 174 }; 175 176 public NotificationStackScrollLayout(Context context) { 177 this(context, null); 178 } 179 180 public NotificationStackScrollLayout(Context context, AttributeSet attrs) { 181 this(context, attrs, 0); 182 } 183 184 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) { 185 this(context, attrs, defStyleAttr, 0); 186 } 187 188 public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr, 189 int defStyleRes) { 190 super(context, attrs, defStyleAttr, defStyleRes); 191 initView(context); 192 if (DEBUG) { 193 setWillNotDraw(false); 194 mDebugPaint = new Paint(); 195 mDebugPaint.setColor(0xffff0000); 196 mDebugPaint.setStrokeWidth(2); 197 mDebugPaint.setStyle(Paint.Style.STROKE); 198 } 199 } 200 201 @Override 202 protected void onDraw(Canvas canvas) { 203 if (DEBUG) { 204 int y = mCollapsedSize; 205 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 206 y = (int) (getLayoutHeight() - mBottomStackPeekSize 207 - mBottomStackSlowDownHeight); 208 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 209 y = (int) (getLayoutHeight() - mBottomStackPeekSize); 210 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 211 y = (int) getLayoutHeight(); 212 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 213 y = getHeight() - getEmptyBottomMargin(); 214 canvas.drawLine(0, y, getWidth(), y, mDebugPaint); 215 } 216 } 217 218 private void initView(Context context) { 219 mScroller = new OverScroller(getContext()); 220 setFocusable(true); 221 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 222 setClipChildren(false); 223 final ViewConfiguration configuration = ViewConfiguration.get(context); 224 mTouchSlop = configuration.getScaledTouchSlop(); 225 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 226 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 227 mOverflingDistance = configuration.getScaledOverflingDistance(); 228 float densityScale = getResources().getDisplayMetrics().density; 229 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 230 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop); 231 232 mSidePaddings = context.getResources() 233 .getDimensionPixelSize(R.dimen.notification_side_padding); 234 mCollapsedSize = context.getResources() 235 .getDimensionPixelSize(R.dimen.notification_min_height); 236 mBottomStackPeekSize = context.getResources() 237 .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); 238 mStackScrollAlgorithm = new StackScrollAlgorithm(context); 239 mPaddingBetweenElementsDimmed = context.getResources() 240 .getDimensionPixelSize(R.dimen.notification_padding_dimmed); 241 mPaddingBetweenElementsNormal = context.getResources() 242 .getDimensionPixelSize(R.dimen.notification_padding); 243 updatePadding(false); 244 int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); 245 int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height); 246 mExpandHelper = new ExpandHelper(getContext(), this, 247 minHeight, maxHeight); 248 mExpandHelper.setEventSource(this); 249 mExpandHelper.setScrollAdapter(this); 250 mMinTopOverScrollToEscape = getResources().getDimensionPixelSize( 251 R.dimen.min_top_overscroll_to_qs); 252 mNotificationTopPadding = getResources().getDimensionPixelSize( 253 R.dimen.notifications_top_padding); 254 mMinStackHeight = getResources().getDimensionPixelSize(R.dimen.collapsed_stack_height); 255 } 256 257 private void updatePadding(boolean dimmed) { 258 mPaddingBetweenElements = dimmed 259 ? mPaddingBetweenElementsDimmed 260 : mPaddingBetweenElementsNormal; 261 mBottomStackSlowDownHeight = mStackScrollAlgorithm.getBottomStackSlowDownLength(); 262 updateContentHeight(); 263 } 264 265 @Override 266 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 267 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 268 int mode = MeasureSpec.getMode(widthMeasureSpec); 269 int size = MeasureSpec.getSize(widthMeasureSpec); 270 int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode); 271 measureChildren(childMeasureSpec, heightMeasureSpec); 272 } 273 274 @Override 275 protected void onLayout(boolean changed, int l, int t, int r, int b) { 276 277 // we layout all our children centered on the top 278 float centerX = getWidth() / 2.0f; 279 for (int i = 0; i < getChildCount(); i++) { 280 View child = getChildAt(i); 281 float width = child.getMeasuredWidth(); 282 float height = child.getMeasuredHeight(); 283 child.layout((int) (centerX - width / 2.0f), 284 0, 285 (int) (centerX + width / 2.0f), 286 (int) height); 287 } 288 setMaxLayoutHeight(getHeight()); 289 updateContentHeight(); 290 updateScrollPositionIfNecessary(); 291 requestChildrenUpdate(); 292 } 293 294 public void updateSpeedBumpIndex(int newIndex) { 295 int currentIndex = indexOfChild(mSpeedBumpView); 296 297 // If we are currently layouted before the new speed bump index, we have to decrease it. 298 boolean validIndex = newIndex > 0; 299 if (newIndex > getChildCount() - 1) { 300 validIndex = false; 301 newIndex = -1; 302 } 303 if (validIndex && currentIndex != newIndex) { 304 changeViewPosition(mSpeedBumpView, newIndex); 305 } 306 updateSpeedBump(validIndex); 307 mAmbientState.setSpeedBumpIndex(newIndex); 308 } 309 310 public void setChildLocationsChangedListener(OnChildLocationsChangedListener listener) { 311 mListener = listener; 312 } 313 314 /** 315 * Returns the location the given child is currently rendered at. 316 * 317 * @param child the child to get the location for 318 * @return one of {@link ViewState}'s <code>LOCATION_*</code> constants 319 */ 320 public int getChildLocation(View child) { 321 ViewState childViewState = mCurrentStackScrollState.getViewStateForView(child); 322 if (childViewState == null) { 323 return ViewState.LOCATION_UNKNOWN; 324 } 325 return childViewState.location; 326 } 327 328 private void setMaxLayoutHeight(int maxLayoutHeight) { 329 mMaxLayoutHeight = maxLayoutHeight; 330 updateAlgorithmHeightAndPadding(); 331 } 332 333 private void updateAlgorithmHeightAndPadding() { 334 mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight()); 335 mStackScrollAlgorithm.setTopPadding(mTopPadding); 336 } 337 338 /** 339 * @return whether the height of the layout needs to be adapted, in order to ensure that the 340 * last child is not in the bottom stack. 341 */ 342 private boolean needsHeightAdaption() { 343 return getNotGoneChildCount() > 1; 344 } 345 346 private boolean isViewExpanded(View view) { 347 if (view != null) { 348 ExpandableView expandView = (ExpandableView) view; 349 return expandView.getActualHeight() > mCollapsedSize; 350 } 351 return false; 352 } 353 354 /** 355 * Updates the children views according to the stack scroll algorithm. Call this whenever 356 * modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout. 357 */ 358 private void updateChildren() { 359 mAmbientState.setScrollY(mOwnScrollY); 360 mStackScrollAlgorithm.getStackScrollState(mAmbientState, mCurrentStackScrollState); 361 if (!isCurrentlyAnimating() && !mNeedsAnimation) { 362 applyCurrentState(); 363 } else { 364 startAnimationToState(); 365 } 366 } 367 368 private void requestChildrenUpdate() { 369 if (!mChildrenUpdateRequested) { 370 getViewTreeObserver().addOnPreDrawListener(mChildrenUpdater); 371 mChildrenUpdateRequested = true; 372 invalidate(); 373 } 374 } 375 376 private boolean isCurrentlyAnimating() { 377 return mStateAnimator.isRunning(); 378 } 379 380 private void updateScrollPositionIfNecessary() { 381 int scrollRange = getScrollRange(); 382 if (scrollRange < mOwnScrollY) { 383 mOwnScrollY = scrollRange; 384 } 385 } 386 387 public int getTopPadding() { 388 return mTopPadding; 389 } 390 391 private void setTopPadding(int topPadding, boolean animate) { 392 if (mTopPadding != topPadding) { 393 mTopPadding = topPadding; 394 updateAlgorithmHeightAndPadding(); 395 updateContentHeight(); 396 if (animate && mAnimationsEnabled && mIsExpanded) { 397 mTopPaddingNeedsAnimation = true; 398 mNeedsAnimation = true; 399 } 400 requestChildrenUpdate(); 401 if (mOnHeightChangedListener != null) { 402 mOnHeightChangedListener.onHeightChanged(null); 403 } 404 } 405 } 406 407 /** 408 * Update the height of the stack to a new height. 409 * 410 * @param height the new height of the stack 411 */ 412 public void setStackHeight(float height) { 413 setIsExpanded(height > 0.0f); 414 int newStackHeight = (int) height; 415 int itemHeight = getItemHeight(); 416 int bottomStackPeekSize = mBottomStackPeekSize; 417 int minStackHeight = itemHeight + bottomStackPeekSize; 418 int stackHeight; 419 if (newStackHeight - mTopPadding >= minStackHeight) { 420 setTranslationY(0); 421 stackHeight = newStackHeight; 422 } else { 423 424 // We did not reach the position yet where we actually start growing, 425 // so we translate the stack upwards. 426 int translationY = (newStackHeight - minStackHeight); 427 // A slight parallax effect is introduced in order for the stack to catch up with 428 // the top card. 429 float partiallyThere = (float) (newStackHeight - mTopPadding) / minStackHeight; 430 partiallyThere = Math.max(0, partiallyThere); 431 translationY += (1 - partiallyThere) * bottomStackPeekSize; 432 setTranslationY(translationY - mTopPadding); 433 stackHeight = (int) (height - (translationY - mTopPadding)); 434 } 435 if (stackHeight != mCurrentStackHeight) { 436 mCurrentStackHeight = stackHeight; 437 updateAlgorithmHeightAndPadding(); 438 requestChildrenUpdate(); 439 } 440 } 441 442 /** 443 * Get the current height of the view. This is at most the msize of the view given by a the 444 * layout but it can also be made smaller by setting {@link #mCurrentStackHeight} 445 * 446 * @return either the layout height or the externally defined height, whichever is smaller 447 */ 448 private int getLayoutHeight() { 449 return Math.min(mMaxLayoutHeight, mCurrentStackHeight); 450 } 451 452 public int getItemHeight() { 453 return mCollapsedSize; 454 } 455 456 public int getBottomStackPeekSize() { 457 return mBottomStackPeekSize; 458 } 459 460 public void setLongPressListener(View.OnLongClickListener listener) { 461 mSwipeHelper.setLongPressListener(listener); 462 } 463 464 public void onChildDismissed(View v) { 465 if (DEBUG) Log.v(TAG, "onChildDismissed: " + v); 466 final View veto = v.findViewById(R.id.veto); 467 if (veto != null && veto.getVisibility() != View.GONE) { 468 veto.performClick(); 469 } 470 setSwipingInProgress(false); 471 if (mDragAnimPendingChildren.contains(v)) { 472 // We start the swipe and finish it in the same frame, we don't want any animation 473 // for the drag 474 mDragAnimPendingChildren.remove(v); 475 } 476 mSwipedOutViews.add(v); 477 mAmbientState.onDragFinished(v); 478 } 479 480 @Override 481 public void onChildSnappedBack(View animView) { 482 mAmbientState.onDragFinished(animView); 483 if (!mDragAnimPendingChildren.contains(animView)) { 484 if (mAnimationsEnabled) { 485 mSnappedBackChildren.add(animView); 486 mNeedsAnimation = true; 487 } 488 requestChildrenUpdate(); 489 } else { 490 // We start the swipe and snap back in the same frame, we don't want any animation 491 mDragAnimPendingChildren.remove(animView); 492 } 493 } 494 495 @Override 496 public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { 497 return false; 498 } 499 500 public void onBeginDrag(View v) { 501 setSwipingInProgress(true); 502 mAmbientState.onBeginDrag(v); 503 if (mAnimationsEnabled) { 504 mDragAnimPendingChildren.add(v); 505 mNeedsAnimation = true; 506 } 507 requestChildrenUpdate(); 508 } 509 510 public void onDragCancelled(View v) { 511 setSwipingInProgress(false); 512 } 513 514 public View getChildAtPosition(MotionEvent ev) { 515 return getChildAtPosition(ev.getX(), ev.getY()); 516 } 517 518 public ExpandableView getChildAtRawPosition(float touchX, float touchY) { 519 int[] location = new int[2]; 520 getLocationOnScreen(location); 521 return getChildAtPosition(touchX - location[0], touchY - location[1]); 522 } 523 524 public ExpandableView getChildAtPosition(float touchX, float touchY) { 525 // find the view under the pointer, accounting for GONE views 526 final int count = getChildCount(); 527 for (int childIdx = 0; childIdx < count; childIdx++) { 528 ExpandableView slidingChild = (ExpandableView) getChildAt(childIdx); 529 if (slidingChild.getVisibility() == GONE) { 530 continue; 531 } 532 float top = slidingChild.getTranslationY(); 533 float bottom = top + slidingChild.getActualHeight(); 534 int left = slidingChild.getLeft(); 535 int right = slidingChild.getRight(); 536 537 if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { 538 return slidingChild; 539 } 540 } 541 return null; 542 } 543 544 public boolean canChildBeExpanded(View v) { 545 return v instanceof ExpandableNotificationRow 546 && ((ExpandableNotificationRow) v).isExpandable(); 547 } 548 549 public void setUserExpandedChild(View v, boolean userExpanded) { 550 if (v instanceof ExpandableNotificationRow) { 551 ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); 552 } 553 } 554 555 public void setUserLockedChild(View v, boolean userLocked) { 556 if (v instanceof ExpandableNotificationRow) { 557 ((ExpandableNotificationRow) v).setUserLocked(userLocked); 558 } 559 removeLongPressCallback(); 560 requestDisallowInterceptTouchEvent(true); 561 } 562 563 @Override 564 public void expansionStateChanged(boolean isExpanding) { 565 mExpandingNotification = isExpanding; 566 if (!mExpandedInThisMotion) { 567 mMaxScrollAfterExpand = mOwnScrollY; 568 mExpandedInThisMotion = true; 569 } 570 } 571 572 public void setScrollingEnabled(boolean enable) { 573 mScrollingEnabled = enable; 574 } 575 576 public void setExpandingEnabled(boolean enable) { 577 mExpandHelper.setEnabled(enable); 578 } 579 580 private boolean isScrollingEnabled() { 581 return mScrollingEnabled; 582 } 583 584 public View getChildContentView(View v) { 585 return v; 586 } 587 588 public boolean canChildBeDismissed(View v) { 589 final View veto = v.findViewById(R.id.veto); 590 return (veto != null && veto.getVisibility() != View.GONE); 591 } 592 593 private void setSwipingInProgress(boolean isSwiped) { 594 mSwipingInProgress = isSwiped; 595 if(isSwiped) { 596 requestDisallowInterceptTouchEvent(true); 597 } 598 } 599 600 @Override 601 protected void onConfigurationChanged(Configuration newConfig) { 602 super.onConfigurationChanged(newConfig); 603 float densityScale = getResources().getDisplayMetrics().density; 604 mSwipeHelper.setDensityScale(densityScale); 605 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 606 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 607 initView(getContext()); 608 } 609 610 public void dismissRowAnimated(View child, int vel) { 611 mSwipeHelper.dismissChild(child, vel); 612 } 613 614 @Override 615 public boolean onTouchEvent(MotionEvent ev) { 616 if (!isEnabled()) { 617 return false; 618 } 619 boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL 620 || ev.getActionMasked()== MotionEvent.ACTION_UP; 621 boolean expandWantsIt = false; 622 if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) { 623 if (isCancelOrUp) { 624 mExpandHelper.onlyObserveMovements(false); 625 } 626 boolean wasExpandingBefore = mExpandingNotification; 627 expandWantsIt = mExpandHelper.onTouchEvent(ev); 628 if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore) { 629 dispatchDownEventToScroller(ev); 630 } 631 } 632 boolean scrollerWantsIt = false; 633 if (!mSwipingInProgress && !mExpandingNotification) { 634 scrollerWantsIt = onScrollTouch(ev); 635 } 636 boolean horizontalSwipeWantsIt = false; 637 if (!mIsBeingDragged 638 && !mExpandingNotification 639 && !mExpandedInThisMotion 640 && !mOnlyScrollingInThisMotion) { 641 horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); 642 } 643 return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev); 644 } 645 646 private void dispatchDownEventToScroller(MotionEvent ev) { 647 MotionEvent downEvent = MotionEvent.obtain(ev); 648 downEvent.setAction(MotionEvent.ACTION_DOWN); 649 onScrollTouch(downEvent); 650 downEvent.recycle(); 651 } 652 653 private boolean onScrollTouch(MotionEvent ev) { 654 if (!isScrollingEnabled()) { 655 return false; 656 } 657 initVelocityTrackerIfNotExists(); 658 mVelocityTracker.addMovement(ev); 659 660 final int action = ev.getAction(); 661 662 switch (action & MotionEvent.ACTION_MASK) { 663 case MotionEvent.ACTION_DOWN: { 664 if (getChildCount() == 0 || !isInContentBounds(ev)) { 665 return false; 666 } 667 boolean isBeingDragged = !mScroller.isFinished(); 668 setIsBeingDragged(isBeingDragged); 669 670 /* 671 * If being flinged and user touches, stop the fling. isFinished 672 * will be false if being flinged. 673 */ 674 if (!mScroller.isFinished()) { 675 mScroller.forceFinished(true); 676 } 677 678 // Remember where the motion event started 679 mLastMotionY = (int) ev.getY(); 680 mDownX = (int) ev.getX(); 681 mActivePointerId = ev.getPointerId(0); 682 break; 683 } 684 case MotionEvent.ACTION_MOVE: 685 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 686 if (activePointerIndex == -1) { 687 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 688 break; 689 } 690 691 final int y = (int) ev.getY(activePointerIndex); 692 final int x = (int) ev.getX(activePointerIndex); 693 int deltaY = mLastMotionY - y; 694 final int xDiff = Math.abs(x - mDownX); 695 final int yDiff = Math.abs(deltaY); 696 if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) { 697 setIsBeingDragged(true); 698 if (deltaY > 0) { 699 deltaY -= mTouchSlop; 700 } else { 701 deltaY += mTouchSlop; 702 } 703 } 704 if (mIsBeingDragged) { 705 // Scroll to follow the motion event 706 mLastMotionY = y; 707 int range = getScrollRange(); 708 if (mExpandedInThisMotion) { 709 range = Math.min(range, mMaxScrollAfterExpand); 710 } 711 712 float scrollAmount; 713 if (deltaY < 0) { 714 scrollAmount = overScrollDown(deltaY); 715 } else { 716 scrollAmount = overScrollUp(deltaY, range); 717 } 718 719 // Calling overScrollBy will call onOverScrolled, which 720 // calls onScrollChanged if applicable. 721 if (scrollAmount != 0.0f) { 722 // The scrolling motion could not be compensated with the 723 // existing overScroll, we have to scroll the view 724 overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY, 725 0, range, 0, getHeight() / 2, true); 726 } 727 } 728 break; 729 case MotionEvent.ACTION_UP: 730 if (mIsBeingDragged) { 731 final VelocityTracker velocityTracker = mVelocityTracker; 732 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 733 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 734 735 if (shouldOverScrollFling(initialVelocity)) { 736 onOverScrollFling(true, initialVelocity); 737 } else { 738 if (getChildCount() > 0) { 739 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 740 float currentOverScrollTop = getCurrentOverScrollAmount(true); 741 if (currentOverScrollTop == 0.0f || initialVelocity > 0) { 742 fling(-initialVelocity); 743 } else { 744 onOverScrollFling(false, initialVelocity); 745 } 746 } else { 747 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, 748 getScrollRange())) { 749 postInvalidateOnAnimation(); 750 } 751 } 752 } 753 754 mActivePointerId = INVALID_POINTER; 755 endDrag(); 756 } 757 } 758 break; 759 case MotionEvent.ACTION_CANCEL: 760 if (mIsBeingDragged && getChildCount() > 0) { 761 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 762 postInvalidateOnAnimation(); 763 } 764 mActivePointerId = INVALID_POINTER; 765 endDrag(); 766 } 767 break; 768 case MotionEvent.ACTION_POINTER_DOWN: { 769 final int index = ev.getActionIndex(); 770 mLastMotionY = (int) ev.getY(index); 771 mDownX = (int) ev.getX(index); 772 mActivePointerId = ev.getPointerId(index); 773 break; 774 } 775 case MotionEvent.ACTION_POINTER_UP: 776 onSecondaryPointerUp(ev); 777 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 778 mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId)); 779 break; 780 } 781 return true; 782 } 783 784 private void onOverScrollFling(boolean open, int initialVelocity) { 785 if (mOverscrollTopChangedListener != null) { 786 mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open); 787 } 788 mDontReportNextOverScroll = true; 789 setOverScrollAmount(0.0f, true, false); 790 } 791 792 /** 793 * Perform a scroll upwards and adapt the overscroll amounts accordingly 794 * 795 * @param deltaY The amount to scroll upwards, has to be positive. 796 * @return The amount of scrolling to be performed by the scroller, 797 * not handled by the overScroll amount. 798 */ 799 private float overScrollUp(int deltaY, int range) { 800 deltaY = Math.max(deltaY, 0); 801 float currentTopAmount = getCurrentOverScrollAmount(true); 802 float newTopAmount = currentTopAmount - deltaY; 803 if (currentTopAmount > 0) { 804 setOverScrollAmount(newTopAmount, true /* onTop */, 805 false /* animate */); 806 } 807 // Top overScroll might not grab all scrolling motion, 808 // we have to scroll as well. 809 float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f; 810 float newScrollY = mOwnScrollY + scrollAmount; 811 if (newScrollY > range) { 812 if (!mExpandedInThisMotion) { 813 float currentBottomPixels = getCurrentOverScrolledPixels(false); 814 // We overScroll on the top 815 setOverScrolledPixels(currentBottomPixels + newScrollY - range, 816 false /* onTop */, 817 false /* animate */); 818 } 819 mOwnScrollY = range; 820 scrollAmount = 0.0f; 821 } 822 return scrollAmount; 823 } 824 825 /** 826 * Perform a scroll downward and adapt the overscroll amounts accordingly 827 * 828 * @param deltaY The amount to scroll downwards, has to be negative. 829 * @return The amount of scrolling to be performed by the scroller, 830 * not handled by the overScroll amount. 831 */ 832 private float overScrollDown(int deltaY) { 833 deltaY = Math.min(deltaY, 0); 834 float currentBottomAmount = getCurrentOverScrollAmount(false); 835 float newBottomAmount = currentBottomAmount + deltaY; 836 if (currentBottomAmount > 0) { 837 setOverScrollAmount(newBottomAmount, false /* onTop */, 838 false /* animate */); 839 } 840 // Bottom overScroll might not grab all scrolling motion, 841 // we have to scroll as well. 842 float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f; 843 float newScrollY = mOwnScrollY + scrollAmount; 844 if (newScrollY < 0) { 845 float currentTopPixels = getCurrentOverScrolledPixels(true); 846 // We overScroll on the top 847 setOverScrolledPixels(currentTopPixels - newScrollY, 848 true /* onTop */, 849 false /* animate */); 850 mOwnScrollY = 0; 851 scrollAmount = 0.0f; 852 } 853 return scrollAmount; 854 } 855 856 private void onSecondaryPointerUp(MotionEvent ev) { 857 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 858 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 859 final int pointerId = ev.getPointerId(pointerIndex); 860 if (pointerId == mActivePointerId) { 861 // This was our active pointer going up. Choose a new 862 // active pointer and adjust accordingly. 863 // TODO: Make this decision more intelligent. 864 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 865 mLastMotionY = (int) ev.getY(newPointerIndex); 866 mActivePointerId = ev.getPointerId(newPointerIndex); 867 if (mVelocityTracker != null) { 868 mVelocityTracker.clear(); 869 } 870 } 871 } 872 873 private void initVelocityTrackerIfNotExists() { 874 if (mVelocityTracker == null) { 875 mVelocityTracker = VelocityTracker.obtain(); 876 } 877 } 878 879 private void recycleVelocityTracker() { 880 if (mVelocityTracker != null) { 881 mVelocityTracker.recycle(); 882 mVelocityTracker = null; 883 } 884 } 885 886 private void initOrResetVelocityTracker() { 887 if (mVelocityTracker == null) { 888 mVelocityTracker = VelocityTracker.obtain(); 889 } else { 890 mVelocityTracker.clear(); 891 } 892 } 893 894 @Override 895 public void computeScroll() { 896 if (mScroller.computeScrollOffset()) { 897 // This is called at drawing time by ViewGroup. 898 int oldX = mScrollX; 899 int oldY = mOwnScrollY; 900 int x = mScroller.getCurrX(); 901 int y = mScroller.getCurrY(); 902 903 if (oldX != x || oldY != y) { 904 final int range = getScrollRange(); 905 if (y < 0 && oldY >= 0 || y > range && oldY <= range) { 906 float currVelocity = mScroller.getCurrVelocity(); 907 if (currVelocity >= mMinimumVelocity) { 908 mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance; 909 } 910 } 911 912 overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 913 0, (int) (mMaxOverScroll), false); 914 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 915 } 916 917 // Keep on drawing until the animation has finished. 918 postInvalidateOnAnimation(); 919 } 920 } 921 922 @Override 923 protected boolean overScrollBy(int deltaX, int deltaY, 924 int scrollX, int scrollY, 925 int scrollRangeX, int scrollRangeY, 926 int maxOverScrollX, int maxOverScrollY, 927 boolean isTouchEvent) { 928 929 int newScrollY = scrollY + deltaY; 930 931 final int top = -maxOverScrollY; 932 final int bottom = maxOverScrollY + scrollRangeY; 933 934 boolean clampedY = false; 935 if (newScrollY > bottom) { 936 newScrollY = bottom; 937 clampedY = true; 938 } else if (newScrollY < top) { 939 newScrollY = top; 940 clampedY = true; 941 } 942 943 onOverScrolled(0, newScrollY, false, clampedY); 944 945 return clampedY; 946 } 947 948 /** 949 * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded 950 * overscroll effect based on numPixels. By default this will also cancel animations on the 951 * same overScroll edge. 952 * 953 * @param numPixels The amount of pixels to overScroll by. These will be scaled according to 954 * the rubber-banding logic. 955 * @param onTop Should the effect be applied on top of the scroller. 956 * @param animate Should an animation be performed. 957 */ 958 public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) { 959 setOverScrollAmount(numPixels * getRubberBandFactor(), onTop, animate, true); 960 } 961 962 /** 963 * Set the effective overScroll amount which will be directly reflected in the layout. 964 * By default this will also cancel animations on the same overScroll edge. 965 * 966 * @param amount The amount to overScroll by. 967 * @param onTop Should the effect be applied on top of the scroller. 968 * @param animate Should an animation be performed. 969 */ 970 public void setOverScrollAmount(float amount, boolean onTop, boolean animate) { 971 setOverScrollAmount(amount, onTop, animate, true); 972 } 973 974 /** 975 * Set the effective overScroll amount which will be directly reflected in the layout. 976 * 977 * @param amount The amount to overScroll by. 978 * @param onTop Should the effect be applied on top of the scroller. 979 * @param animate Should an animation be performed. 980 * @param cancelAnimators Should running animations be cancelled. 981 */ 982 public void setOverScrollAmount(float amount, boolean onTop, boolean animate, 983 boolean cancelAnimators) { 984 if (cancelAnimators) { 985 mStateAnimator.cancelOverScrollAnimators(onTop); 986 } 987 setOverScrollAmountInternal(amount, onTop, animate); 988 } 989 990 private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate) { 991 amount = Math.max(0, amount); 992 if (animate) { 993 mStateAnimator.animateOverScrollToAmount(amount, onTop); 994 } else { 995 setOverScrolledPixels(amount / getRubberBandFactor(), onTop); 996 mAmbientState.setOverScrollAmount(amount, onTop); 997 if (onTop) { 998 notifyOverscrollTopListener(amount); 999 } 1000 requestChildrenUpdate(); 1001 } 1002 } 1003 1004 private void notifyOverscrollTopListener(float amount) { 1005 mExpandHelper.onlyObserveMovements(amount > 1.0f); 1006 if (mDontReportNextOverScroll) { 1007 mDontReportNextOverScroll = false; 1008 return; 1009 } 1010 if (mOverscrollTopChangedListener != null) { 1011 mOverscrollTopChangedListener.onOverscrollTopChanged(amount); 1012 } 1013 } 1014 1015 public void setOverscrollTopChangedListener( 1016 OnOverscrollTopChangedListener overscrollTopChangedListener) { 1017 mOverscrollTopChangedListener = overscrollTopChangedListener; 1018 } 1019 1020 public float getCurrentOverScrollAmount(boolean top) { 1021 return mAmbientState.getOverScrollAmount(top); 1022 } 1023 1024 public float getCurrentOverScrolledPixels(boolean top) { 1025 return top? mOverScrolledTopPixels : mOverScrolledBottomPixels; 1026 } 1027 1028 private void setOverScrolledPixels(float amount, boolean onTop) { 1029 if (onTop) { 1030 mOverScrolledTopPixels = amount; 1031 } else { 1032 mOverScrolledBottomPixels = amount; 1033 } 1034 } 1035 1036 private void customScrollTo(int y) { 1037 mOwnScrollY = y; 1038 updateChildren(); 1039 } 1040 1041 @Override 1042 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 1043 // Treat animating scrolls differently; see #computeScroll() for why. 1044 if (!mScroller.isFinished()) { 1045 final int oldX = mScrollX; 1046 final int oldY = mOwnScrollY; 1047 mScrollX = scrollX; 1048 mOwnScrollY = scrollY; 1049 if (clampedY) { 1050 springBack(); 1051 } else { 1052 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 1053 invalidateParentIfNeeded(); 1054 updateChildren(); 1055 float overScrollTop = getCurrentOverScrollAmount(true); 1056 if (mOwnScrollY < 0) { 1057 notifyOverscrollTopListener(-mOwnScrollY); 1058 } else { 1059 notifyOverscrollTopListener(overScrollTop); 1060 } 1061 } 1062 } else { 1063 customScrollTo(scrollY); 1064 scrollTo(scrollX, mScrollY); 1065 } 1066 } 1067 1068 private void springBack() { 1069 int scrollRange = getScrollRange(); 1070 boolean overScrolledTop = mOwnScrollY <= 0; 1071 boolean overScrolledBottom = mOwnScrollY >= scrollRange; 1072 if (overScrolledTop || overScrolledBottom) { 1073 boolean onTop; 1074 float newAmount; 1075 if (overScrolledTop) { 1076 onTop = true; 1077 newAmount = -mOwnScrollY; 1078 mOwnScrollY = 0; 1079 mDontReportNextOverScroll = true; 1080 } else { 1081 onTop = false; 1082 newAmount = mOwnScrollY - scrollRange; 1083 mOwnScrollY = scrollRange; 1084 } 1085 setOverScrollAmount(newAmount, onTop, false); 1086 setOverScrollAmount(0.0f, onTop, true); 1087 mScroller.forceFinished(true); 1088 } 1089 } 1090 1091 private int getScrollRange() { 1092 int scrollRange = 0; 1093 ExpandableView firstChild = (ExpandableView) getFirstChildNotGone(); 1094 if (firstChild != null) { 1095 int contentHeight = getContentHeight(); 1096 int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild); 1097 scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize 1098 + mBottomStackSlowDownHeight); 1099 if (scrollRange > 0) { 1100 View lastChild = getLastChildNotGone(); 1101 // We want to at least be able collapse the first item and not ending in a weird 1102 // end state. 1103 scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize); 1104 } 1105 } 1106 return scrollRange; 1107 } 1108 1109 /** 1110 * @return the first child which has visibility unequal to GONE 1111 */ 1112 private View getFirstChildNotGone() { 1113 int childCount = getChildCount(); 1114 for (int i = 0; i < childCount; i++) { 1115 View child = getChildAt(i); 1116 if (child.getVisibility() != View.GONE) { 1117 return child; 1118 } 1119 } 1120 return null; 1121 } 1122 1123 /** 1124 * @return The first child which has visibility unequal to GONE which is currently below the 1125 * given translationY or equal to it. 1126 */ 1127 private View getFirstChildBelowTranlsationY(float translationY) { 1128 int childCount = getChildCount(); 1129 for (int i = 0; i < childCount; i++) { 1130 View child = getChildAt(i); 1131 if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) { 1132 return child; 1133 } 1134 } 1135 return null; 1136 } 1137 1138 /** 1139 * @return the last child which has visibility unequal to GONE 1140 */ 1141 public View getLastChildNotGone() { 1142 int childCount = getChildCount(); 1143 for (int i = childCount - 1; i >= 0; i--) { 1144 View child = getChildAt(i); 1145 if (child.getVisibility() != View.GONE) { 1146 return child; 1147 } 1148 } 1149 return null; 1150 } 1151 1152 /** 1153 * @return the number of children which have visibility unequal to GONE 1154 */ 1155 public int getNotGoneChildCount() { 1156 int childCount = getChildCount(); 1157 int count = 0; 1158 for (int i = 0; i < childCount; i++) { 1159 View child = getChildAt(i); 1160 if (child.getVisibility() != View.GONE) { 1161 count++; 1162 } 1163 } 1164 return count; 1165 } 1166 1167 private int getMaxExpandHeight(View view) { 1168 if (view instanceof ExpandableNotificationRow) { 1169 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 1170 return row.getIntrinsicHeight(); 1171 } 1172 return view.getHeight(); 1173 } 1174 1175 private int getContentHeight() { 1176 return mContentHeight; 1177 } 1178 1179 private void updateContentHeight() { 1180 int height = 0; 1181 for (int i = 0; i < getChildCount(); i++) { 1182 View child = getChildAt(i); 1183 if (child.getVisibility() != View.GONE) { 1184 if (height != 0) { 1185 // add the padding before this element 1186 height += mPaddingBetweenElements; 1187 } 1188 if (child instanceof ExpandableNotificationRow) { 1189 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 1190 height += row.getIntrinsicHeight(); 1191 } else if (child instanceof ExpandableView) { 1192 ExpandableView expandableView = (ExpandableView) child; 1193 height += expandableView.getActualHeight(); 1194 } 1195 } 1196 } 1197 mContentHeight = height + mTopPadding; 1198 } 1199 1200 /** 1201 * Fling the scroll view 1202 * 1203 * @param velocityY The initial velocity in the Y direction. Positive 1204 * numbers mean that the finger/cursor is moving down the screen, 1205 * which means we want to scroll towards the top. 1206 */ 1207 private void fling(int velocityY) { 1208 if (getChildCount() > 0) { 1209 int scrollRange = getScrollRange(); 1210 1211 float topAmount = getCurrentOverScrollAmount(true); 1212 float bottomAmount = getCurrentOverScrollAmount(false); 1213 if (velocityY < 0 && topAmount > 0) { 1214 mOwnScrollY -= (int) topAmount; 1215 mDontReportNextOverScroll = true; 1216 setOverScrollAmount(0, true, false); 1217 mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor() 1218 * mOverflingDistance + topAmount; 1219 } else if (velocityY > 0 && bottomAmount > 0) { 1220 mOwnScrollY += bottomAmount; 1221 setOverScrollAmount(0, false, false); 1222 mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor() 1223 * mOverflingDistance + bottomAmount; 1224 } else { 1225 // it will be set once we reach the boundary 1226 mMaxOverScroll = 0.0f; 1227 } 1228 mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0, 1229 Math.max(0, scrollRange), 0, Integer.MAX_VALUE / 2); 1230 1231 postInvalidateOnAnimation(); 1232 } 1233 } 1234 1235 /** 1236 * @return Whether a fling performed on the top overscroll edge lead to the expanded 1237 * overScroll view (i.e QS). 1238 */ 1239 private boolean shouldOverScrollFling(int initialVelocity) { 1240 float topOverScroll = getCurrentOverScrollAmount(true); 1241 return mScrolledToTopOnFirstDown 1242 && !mExpandedInThisMotion 1243 && topOverScroll > mMinTopOverScrollToEscape 1244 && initialVelocity > 0; 1245 } 1246 1247 public void updateTopPadding(float qsHeight, int scrollY, boolean animate) { 1248 float start = qsHeight - scrollY + mNotificationTopPadding; 1249 float stackHeight = getHeight() - start; 1250 if (stackHeight <= mMinStackHeight) { 1251 float overflow = mMinStackHeight - stackHeight; 1252 stackHeight = mMinStackHeight; 1253 start = getHeight() - stackHeight; 1254 setTranslationY(overflow); 1255 } else { 1256 setTranslationY(0); 1257 } 1258 setTopPadding(clampPadding((int) start), animate); 1259 } 1260 1261 private int clampPadding(int desiredPadding) { 1262 return Math.max(desiredPadding, mIntrinsicPadding); 1263 } 1264 1265 private float getRubberBandFactor() { 1266 return mExpandedInThisMotion 1267 ? RUBBER_BAND_FACTOR_AFTER_EXPAND 1268 : (mScrolledToTopOnFirstDown 1269 ? 1.0f 1270 : RUBBER_BAND_FACTOR_NORMAL); 1271 } 1272 1273 private void endDrag() { 1274 setIsBeingDragged(false); 1275 1276 recycleVelocityTracker(); 1277 1278 if (getCurrentOverScrollAmount(true /* onTop */) > 0) { 1279 setOverScrollAmount(0, true /* onTop */, true /* animate */); 1280 } 1281 if (getCurrentOverScrollAmount(false /* onTop */) > 0) { 1282 setOverScrollAmount(0, false /* onTop */, true /* animate */); 1283 } 1284 } 1285 1286 @Override 1287 public boolean onInterceptTouchEvent(MotionEvent ev) { 1288 initDownStates(ev); 1289 boolean expandWantsIt = false; 1290 if (!mSwipingInProgress && !mOnlyScrollingInThisMotion) { 1291 expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev); 1292 } 1293 boolean scrollWantsIt = false; 1294 if (!mSwipingInProgress && !mExpandingNotification) { 1295 scrollWantsIt = onInterceptTouchEventScroll(ev); 1296 } 1297 boolean swipeWantsIt = false; 1298 if (!mIsBeingDragged 1299 && !mExpandingNotification 1300 && !mExpandedInThisMotion 1301 && !mOnlyScrollingInThisMotion) { 1302 swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); 1303 } 1304 return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev); 1305 } 1306 1307 private void initDownStates(MotionEvent ev) { 1308 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 1309 mExpandedInThisMotion = false; 1310 mOnlyScrollingInThisMotion = !mScroller.isFinished(); 1311 } 1312 } 1313 1314 @Override 1315 protected void onViewRemoved(View child) { 1316 super.onViewRemoved(child); 1317 mStackScrollAlgorithm.notifyChildrenChanged(this); 1318 if (mChildrenChangingPositions.contains(child)) { 1319 // This is only a position change, don't do anything special 1320 return; 1321 } 1322 ((ExpandableView) child).setOnHeightChangedListener(null); 1323 mCurrentStackScrollState.removeViewStateForView(child); 1324 updateScrollStateForRemovedChild(child); 1325 boolean animationGenerated = generateRemoveAnimation(child); 1326 if (animationGenerated && !mSwipedOutViews.contains(child)) { 1327 // Add this view to an overlay in order to ensure that it will still be temporary 1328 // drawn when removed 1329 getOverlay().add(child); 1330 } 1331 } 1332 1333 /** 1334 * Generate a remove animation for a child view. 1335 * 1336 * @param child The view to generate the remove animation for. 1337 * @return Whether an animation was generated. 1338 */ 1339 private boolean generateRemoveAnimation(View child) { 1340 if (mIsExpanded && mAnimationsEnabled) { 1341 if (!mChildrenToAddAnimated.contains(child)) { 1342 // Generate Animations 1343 mChildrenToRemoveAnimated.add(child); 1344 mNeedsAnimation = true; 1345 return true; 1346 } else { 1347 mChildrenToAddAnimated.remove(child); 1348 return false; 1349 } 1350 } 1351 return false; 1352 } 1353 1354 /** 1355 * Updates the scroll position when a child was removed 1356 * 1357 * @param removedChild the removed child 1358 */ 1359 private void updateScrollStateForRemovedChild(View removedChild) { 1360 int startingPosition = getPositionInLinearLayout(removedChild); 1361 int childHeight = removedChild.getHeight() + mPaddingBetweenElements; 1362 int endPosition = startingPosition + childHeight; 1363 if (endPosition <= mOwnScrollY) { 1364 // This child is fully scrolled of the top, so we have to deduct its height from the 1365 // scrollPosition 1366 mOwnScrollY -= childHeight; 1367 } else if (startingPosition < mOwnScrollY) { 1368 // This child is currently being scrolled into, set the scroll position to the start of 1369 // this child 1370 mOwnScrollY = startingPosition; 1371 } 1372 } 1373 1374 private int getPositionInLinearLayout(View requestedChild) { 1375 int position = 0; 1376 for (int i = 0; i < getChildCount(); i++) { 1377 View child = getChildAt(i); 1378 if (child == requestedChild) { 1379 return position; 1380 } 1381 if (child.getVisibility() != View.GONE) { 1382 position += child.getHeight(); 1383 if (i < getChildCount()-1) { 1384 position += mPaddingBetweenElements; 1385 } 1386 } 1387 } 1388 return 0; 1389 } 1390 1391 @Override 1392 protected void onViewAdded(View child) { 1393 super.onViewAdded(child); 1394 mStackScrollAlgorithm.notifyChildrenChanged(this); 1395 ((ExpandableView) child).setOnHeightChangedListener(this); 1396 generateAddAnimation(child); 1397 } 1398 1399 public void setAnimationsEnabled(boolean animationsEnabled) { 1400 mAnimationsEnabled = animationsEnabled; 1401 } 1402 1403 public boolean isAddOrRemoveAnimationPending() { 1404 return mNeedsAnimation 1405 && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty()); 1406 } 1407 /** 1408 * Generate an animation for an added child view. 1409 * 1410 * @param child The view to be added. 1411 */ 1412 public void generateAddAnimation(View child) { 1413 if (mIsExpanded && mAnimationsEnabled && !mChildrenChangingPositions.contains(child)) { 1414 // Generate Animations 1415 mChildrenToAddAnimated.add(child); 1416 mNeedsAnimation = true; 1417 } 1418 } 1419 1420 /** 1421 * Change the position of child to a new location 1422 * 1423 * @param child the view to change the position for 1424 * @param newIndex the new index 1425 */ 1426 public void changeViewPosition(View child, int newIndex) { 1427 if (child != null && child.getParent() == this) { 1428 mChildrenChangingPositions.add(child); 1429 removeView(child); 1430 addView(child, newIndex); 1431 mNeedsAnimation = true; 1432 } 1433 } 1434 1435 private void startAnimationToState() { 1436 if (mNeedsAnimation) { 1437 generateChildHierarchyEvents(); 1438 mNeedsAnimation = false; 1439 } 1440 if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) { 1441 mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState); 1442 mAnimationEvents.clear(); 1443 } else { 1444 applyCurrentState(); 1445 } 1446 } 1447 1448 private void generateChildHierarchyEvents() { 1449 generateChildRemovalEvents(); 1450 generateChildAdditionEvents(); 1451 generatePositionChangeEvents(); 1452 generateSnapBackEvents(); 1453 generateDragEvents(); 1454 generateTopPaddingEvent(); 1455 generateActivateEvent(); 1456 generateDimmedEvent(); 1457 mNeedsAnimation = false; 1458 } 1459 1460 private void generateSnapBackEvents() { 1461 for (View child : mSnappedBackChildren) { 1462 mAnimationEvents.add(new AnimationEvent(child, 1463 AnimationEvent.ANIMATION_TYPE_SNAP_BACK)); 1464 } 1465 mSnappedBackChildren.clear(); 1466 } 1467 1468 private void generateDragEvents() { 1469 for (View child : mDragAnimPendingChildren) { 1470 mAnimationEvents.add(new AnimationEvent(child, 1471 AnimationEvent.ANIMATION_TYPE_START_DRAG)); 1472 } 1473 mDragAnimPendingChildren.clear(); 1474 } 1475 1476 private void generateChildRemovalEvents() { 1477 for (View child : mChildrenToRemoveAnimated) { 1478 boolean childWasSwipedOut = mSwipedOutViews.contains(child); 1479 int animationType = childWasSwipedOut 1480 ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT 1481 : AnimationEvent.ANIMATION_TYPE_REMOVE; 1482 AnimationEvent event = new AnimationEvent(child, animationType); 1483 1484 // we need to know the view after this one 1485 event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY()); 1486 mAnimationEvents.add(event); 1487 } 1488 mSwipedOutViews.clear(); 1489 mChildrenToRemoveAnimated.clear(); 1490 } 1491 1492 private void generatePositionChangeEvents() { 1493 for (View child : mChildrenChangingPositions) { 1494 mAnimationEvents.add(new AnimationEvent(child, 1495 AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)); 1496 } 1497 mChildrenChangingPositions.clear(); 1498 } 1499 1500 private void generateChildAdditionEvents() { 1501 for (View child : mChildrenToAddAnimated) { 1502 mAnimationEvents.add(new AnimationEvent(child, 1503 AnimationEvent.ANIMATION_TYPE_ADD)); 1504 } 1505 mChildrenToAddAnimated.clear(); 1506 } 1507 1508 private void generateTopPaddingEvent() { 1509 if (mTopPaddingNeedsAnimation) { 1510 mAnimationEvents.add( 1511 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED)); 1512 } 1513 mTopPaddingNeedsAnimation = false; 1514 } 1515 1516 private void generateActivateEvent() { 1517 if (mActivateNeedsAnimation) { 1518 mAnimationEvents.add( 1519 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD)); 1520 } 1521 mActivateNeedsAnimation = false; 1522 } 1523 1524 private void generateDimmedEvent() { 1525 if (mDimmedNeedsAnimation) { 1526 mAnimationEvents.add( 1527 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED)); 1528 } 1529 mDimmedNeedsAnimation = false; 1530 } 1531 1532 private boolean onInterceptTouchEventScroll(MotionEvent ev) { 1533 if (!isScrollingEnabled()) { 1534 return false; 1535 } 1536 /* 1537 * This method JUST determines whether we want to intercept the motion. 1538 * If we return true, onMotionEvent will be called and we do the actual 1539 * scrolling there. 1540 */ 1541 1542 /* 1543 * Shortcut the most recurring case: the user is in the dragging 1544 * state and he is moving his finger. We want to intercept this 1545 * motion. 1546 */ 1547 final int action = ev.getAction(); 1548 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 1549 return true; 1550 } 1551 1552 switch (action & MotionEvent.ACTION_MASK) { 1553 case MotionEvent.ACTION_MOVE: { 1554 /* 1555 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1556 * whether the user has moved far enough from his original down touch. 1557 */ 1558 1559 /* 1560 * Locally do absolute value. mLastMotionY is set to the y value 1561 * of the down event. 1562 */ 1563 final int activePointerId = mActivePointerId; 1564 if (activePointerId == INVALID_POINTER) { 1565 // If we don't have a valid id, the touch down wasn't on content. 1566 break; 1567 } 1568 1569 final int pointerIndex = ev.findPointerIndex(activePointerId); 1570 if (pointerIndex == -1) { 1571 Log.e(TAG, "Invalid pointerId=" + activePointerId 1572 + " in onInterceptTouchEvent"); 1573 break; 1574 } 1575 1576 final int y = (int) ev.getY(pointerIndex); 1577 final int x = (int) ev.getX(pointerIndex); 1578 final int yDiff = Math.abs(y - mLastMotionY); 1579 final int xDiff = Math.abs(x - mDownX); 1580 if (yDiff > mTouchSlop && yDiff > xDiff) { 1581 setIsBeingDragged(true); 1582 mLastMotionY = y; 1583 mDownX = x; 1584 initVelocityTrackerIfNotExists(); 1585 mVelocityTracker.addMovement(ev); 1586 } 1587 break; 1588 } 1589 1590 case MotionEvent.ACTION_DOWN: { 1591 final int y = (int) ev.getY(); 1592 if (getChildAtPosition(ev.getX(), y) == null) { 1593 setIsBeingDragged(false); 1594 recycleVelocityTracker(); 1595 break; 1596 } 1597 1598 /* 1599 * Remember location of down touch. 1600 * ACTION_DOWN always refers to pointer index 0. 1601 */ 1602 mLastMotionY = y; 1603 mDownX = (int) ev.getX(); 1604 mActivePointerId = ev.getPointerId(0); 1605 mScrolledToTopOnFirstDown = isScrolledToTop(); 1606 1607 initOrResetVelocityTracker(); 1608 mVelocityTracker.addMovement(ev); 1609 /* 1610 * If being flinged and user touches the screen, initiate drag; 1611 * otherwise don't. mScroller.isFinished should be false when 1612 * being flinged. 1613 */ 1614 boolean isBeingDragged = !mScroller.isFinished(); 1615 setIsBeingDragged(isBeingDragged); 1616 break; 1617 } 1618 1619 case MotionEvent.ACTION_CANCEL: 1620 case MotionEvent.ACTION_UP: 1621 /* Release the drag */ 1622 setIsBeingDragged(false); 1623 mActivePointerId = INVALID_POINTER; 1624 recycleVelocityTracker(); 1625 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 1626 postInvalidateOnAnimation(); 1627 } 1628 break; 1629 case MotionEvent.ACTION_POINTER_UP: 1630 onSecondaryPointerUp(ev); 1631 break; 1632 } 1633 1634 /* 1635 * The only time we want to intercept motion events is if we are in the 1636 * drag mode. 1637 */ 1638 return mIsBeingDragged; 1639 } 1640 1641 /** 1642 * @return Whether the specified motion event is actually happening over the content. 1643 */ 1644 private boolean isInContentBounds(MotionEvent event) { 1645 return event.getY() < getHeight() - getEmptyBottomMargin(); 1646 } 1647 1648 private void setIsBeingDragged(boolean isDragged) { 1649 mIsBeingDragged = isDragged; 1650 if (isDragged) { 1651 requestDisallowInterceptTouchEvent(true); 1652 removeLongPressCallback(); 1653 } 1654 } 1655 1656 @Override 1657 public void onWindowFocusChanged(boolean hasWindowFocus) { 1658 super.onWindowFocusChanged(hasWindowFocus); 1659 if (!hasWindowFocus) { 1660 removeLongPressCallback(); 1661 } 1662 } 1663 1664 public void removeLongPressCallback() { 1665 mSwipeHelper.removeLongPressCallback(); 1666 } 1667 1668 @Override 1669 public boolean isScrolledToTop() { 1670 return mOwnScrollY == 0; 1671 } 1672 1673 @Override 1674 public boolean isScrolledToBottom() { 1675 return mOwnScrollY >= getScrollRange(); 1676 } 1677 1678 @Override 1679 public View getHostView() { 1680 return this; 1681 } 1682 1683 public int getEmptyBottomMargin() { 1684 int emptyMargin = mMaxLayoutHeight - mContentHeight; 1685 if (needsHeightAdaption()) { 1686 emptyMargin = emptyMargin - mBottomStackSlowDownHeight - mBottomStackPeekSize; 1687 } else { 1688 emptyMargin = emptyMargin - mBottomStackPeekSize; 1689 } 1690 return Math.max(emptyMargin, 0); 1691 } 1692 1693 public void onExpansionStarted() { 1694 mIsExpansionChanging = true; 1695 mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); 1696 } 1697 1698 public void onExpansionStopped() { 1699 mIsExpansionChanging = false; 1700 mStackScrollAlgorithm.onExpansionStopped(); 1701 } 1702 1703 private void setIsExpanded(boolean isExpanded) { 1704 mIsExpanded = isExpanded; 1705 mStackScrollAlgorithm.setIsExpanded(isExpanded); 1706 if (!isExpanded) { 1707 mOwnScrollY = 0; 1708 mSpeedBumpView.collapse(); 1709 } 1710 } 1711 1712 @Override 1713 public void onHeightChanged(ExpandableView view) { 1714 updateContentHeight(); 1715 updateScrollPositionIfNecessary(); 1716 if (mOnHeightChangedListener != null) { 1717 mOnHeightChangedListener.onHeightChanged(view); 1718 } 1719 requestChildrenUpdate(); 1720 } 1721 1722 public void setOnHeightChangedListener( 1723 ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { 1724 this.mOnHeightChangedListener = mOnHeightChangedListener; 1725 } 1726 1727 public void onChildAnimationFinished() { 1728 requestChildrenUpdate(); 1729 } 1730 1731 /** 1732 * See {@link AmbientState#setDimmed}. 1733 */ 1734 public void setDimmed(boolean dimmed, boolean animate) { 1735 mStackScrollAlgorithm.setDimmed(dimmed); 1736 mAmbientState.setDimmed(dimmed); 1737 updatePadding(dimmed); 1738 if (animate && mAnimationsEnabled) { 1739 mDimmedNeedsAnimation = true; 1740 mNeedsAnimation = true; 1741 } 1742 requestChildrenUpdate(); 1743 } 1744 1745 /** 1746 * See {@link AmbientState#setActivatedChild}. 1747 */ 1748 public void setActivatedChild(View activatedChild) { 1749 mAmbientState.setActivatedChild(activatedChild); 1750 if (mAnimationsEnabled) { 1751 mActivateNeedsAnimation = true; 1752 mNeedsAnimation = true; 1753 } 1754 requestChildrenUpdate(); 1755 } 1756 1757 public View getActivatedChild() { 1758 return mAmbientState.getActivatedChild(); 1759 } 1760 1761 private void applyCurrentState() { 1762 mCurrentStackScrollState.apply(); 1763 if (mListener != null) { 1764 mListener.onChildLocationsChanged(this); 1765 } 1766 } 1767 1768 public void setSpeedBumpView(SpeedBumpView speedBumpView) { 1769 mSpeedBumpView = speedBumpView; 1770 addView(speedBumpView); 1771 } 1772 1773 private void updateSpeedBump(boolean visible) { 1774 boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE; 1775 if (visible != notGoneBefore) { 1776 int newVisibility = visible ? VISIBLE : GONE; 1777 mSpeedBumpView.setVisibility(newVisibility); 1778 if (visible) { 1779 mSpeedBumpView.collapse(); 1780 // Make invisible to ensure that the appear animation is played. 1781 mSpeedBumpView.setInvisible(); 1782 if (!mIsExpansionChanging) { 1783 generateAddAnimation(mSpeedBumpView); 1784 } 1785 } else { 1786 mSpeedBumpView.performVisibilityAnimation(false); 1787 generateRemoveAnimation(mSpeedBumpView); 1788 } 1789 } 1790 } 1791 1792 public void goToFullShade() { 1793 updateSpeedBump(true); 1794 } 1795 1796 public void cancelExpandHelper() { 1797 mExpandHelper.cancel(); 1798 } 1799 1800 public void setIntrinsicPadding(int intrinsicPadding) { 1801 mIntrinsicPadding = intrinsicPadding; 1802 } 1803 1804 /** 1805 * @return the y position of the first notification 1806 */ 1807 public float getNotificationsTopY() { 1808 return mTopPadding + getTranslationY(); 1809 } 1810 1811 /** 1812 * A listener that is notified when some child locations might have changed. 1813 */ 1814 public interface OnChildLocationsChangedListener { 1815 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); 1816 } 1817 1818 /** 1819 * A listener that gets notified when the overscroll at the top has changed. 1820 */ 1821 public interface OnOverscrollTopChangedListener { 1822 public void onOverscrollTopChanged(float amount); 1823 1824 /** 1825 * Notify a listener that the scroller wants to escape from the scrolling motion and 1826 * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS) 1827 * 1828 * @param velocity The velocity that the Scroller had when over flinging 1829 * @param open Should the fling open or close the overscroll view. 1830 */ 1831 public void flingTopOverscroll(float velocity, boolean open); 1832 } 1833 1834 static class AnimationEvent { 1835 1836 static AnimationFilter[] FILTERS = new AnimationFilter[] { 1837 1838 // ANIMATION_TYPE_ADD 1839 new AnimationFilter() 1840 .animateAlpha() 1841 .animateHeight() 1842 .animateTopInset() 1843 .animateY() 1844 .animateZ() 1845 .hasDelays(), 1846 1847 // ANIMATION_TYPE_REMOVE 1848 new AnimationFilter() 1849 .animateAlpha() 1850 .animateHeight() 1851 .animateTopInset() 1852 .animateY() 1853 .animateZ() 1854 .hasDelays(), 1855 1856 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 1857 new AnimationFilter() 1858 .animateAlpha() 1859 .animateHeight() 1860 .animateTopInset() 1861 .animateY() 1862 .animateZ() 1863 .hasDelays(), 1864 1865 // ANIMATION_TYPE_TOP_PADDING_CHANGED 1866 new AnimationFilter() 1867 .animateAlpha() 1868 .animateHeight() 1869 .animateTopInset() 1870 .animateY() 1871 .animateDimmed() 1872 .animateScale() 1873 .animateZ(), 1874 1875 // ANIMATION_TYPE_START_DRAG 1876 new AnimationFilter() 1877 .animateAlpha(), 1878 1879 // ANIMATION_TYPE_SNAP_BACK 1880 new AnimationFilter() 1881 .animateAlpha(), 1882 1883 // ANIMATION_TYPE_ACTIVATED_CHILD 1884 new AnimationFilter() 1885 .animateScale() 1886 .animateAlpha(), 1887 1888 // ANIMATION_TYPE_DIMMED 1889 new AnimationFilter() 1890 .animateY() 1891 .animateScale() 1892 .animateDimmed(), 1893 1894 // ANIMATION_TYPE_CHANGE_POSITION 1895 new AnimationFilter() 1896 .animateAlpha() 1897 .animateHeight() 1898 .animateTopInset() 1899 .animateY() 1900 .animateZ() 1901 }; 1902 1903 static int[] LENGTHS = new int[] { 1904 1905 // ANIMATION_TYPE_ADD 1906 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 1907 1908 // ANIMATION_TYPE_REMOVE 1909 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 1910 1911 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 1912 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1913 1914 // ANIMATION_TYPE_TOP_PADDING_CHANGED 1915 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1916 1917 // ANIMATION_TYPE_START_DRAG 1918 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1919 1920 // ANIMATION_TYPE_SNAP_BACK 1921 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1922 1923 // ANIMATION_TYPE_ACTIVATED_CHILD 1924 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 1925 1926 // ANIMATION_TYPE_DIMMED 1927 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 1928 1929 // ANIMATION_TYPE_CHANGE_POSITION 1930 StackStateAnimator.ANIMATION_DURATION_STANDARD, 1931 }; 1932 1933 static final int ANIMATION_TYPE_ADD = 0; 1934 static final int ANIMATION_TYPE_REMOVE = 1; 1935 static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2; 1936 static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3; 1937 static final int ANIMATION_TYPE_START_DRAG = 4; 1938 static final int ANIMATION_TYPE_SNAP_BACK = 5; 1939 static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6; 1940 static final int ANIMATION_TYPE_DIMMED = 7; 1941 static final int ANIMATION_TYPE_CHANGE_POSITION = 8; 1942 1943 final long eventStartTime; 1944 final View changingView; 1945 final int animationType; 1946 final AnimationFilter filter; 1947 final long length; 1948 View viewAfterChangingView; 1949 1950 AnimationEvent(View view, int type) { 1951 eventStartTime = AnimationUtils.currentAnimationTimeMillis(); 1952 changingView = view; 1953 animationType = type; 1954 filter = FILTERS[type]; 1955 length = LENGTHS[type]; 1956 } 1957 1958 /** 1959 * Combines the length of several animation events into a single value. 1960 * 1961 * @param events The events of the lengths to combine. 1962 * @return The combined length. This is just the maximum of all length. 1963 */ 1964 static long combineLength(ArrayList<AnimationEvent> events) { 1965 long length = 0; 1966 int size = events.size(); 1967 for (int i = 0; i < size; i++) { 1968 length = Math.max(length, events.get(i).length); 1969 } 1970 return length; 1971 } 1972 } 1973 1974} 1975