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