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