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