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