NotificationStackScrollLayout.java revision 06c19ea00190e69496be048758101f84f3bc43ad
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 updateAnimationState(false, child); 1471 1472 // Make sure the clipRect we might have set is removed 1473 child.setClipBounds(null); 1474 } 1475 1476 /** 1477 * Generate a remove animation for a child view. 1478 * 1479 * @param child The view to generate the remove animation for. 1480 * @return Whether an animation was generated. 1481 */ 1482 private boolean generateRemoveAnimation(View child) { 1483 if (mIsExpanded && mAnimationsEnabled) { 1484 if (!mChildrenToAddAnimated.contains(child)) { 1485 // Generate Animations 1486 mChildrenToRemoveAnimated.add(child); 1487 mNeedsAnimation = true; 1488 return true; 1489 } else { 1490 mChildrenToAddAnimated.remove(child); 1491 mFromMoreCardAdditions.remove(child); 1492 return false; 1493 } 1494 } 1495 return false; 1496 } 1497 1498 /** 1499 * Updates the scroll position when a child was removed 1500 * 1501 * @param removedChild the removed child 1502 */ 1503 private void updateScrollStateForRemovedChild(View removedChild) { 1504 int startingPosition = getPositionInLinearLayout(removedChild); 1505 int childHeight = getIntrinsicHeight(removedChild) + mPaddingBetweenElements; 1506 int endPosition = startingPosition + childHeight; 1507 if (endPosition <= mOwnScrollY) { 1508 // This child is fully scrolled of the top, so we have to deduct its height from the 1509 // scrollPosition 1510 mOwnScrollY -= childHeight; 1511 } else if (startingPosition < mOwnScrollY) { 1512 // This child is currently being scrolled into, set the scroll position to the start of 1513 // this child 1514 mOwnScrollY = startingPosition; 1515 } 1516 } 1517 1518 private int getIntrinsicHeight(View view) { 1519 if (view instanceof ExpandableView) { 1520 ExpandableView expandableView = (ExpandableView) view; 1521 return expandableView.getIntrinsicHeight(); 1522 } 1523 return view.getHeight(); 1524 } 1525 1526 private int getPositionInLinearLayout(View requestedChild) { 1527 int position = 0; 1528 for (int i = 0; i < getChildCount(); i++) { 1529 View child = getChildAt(i); 1530 if (child == requestedChild) { 1531 return position; 1532 } 1533 if (child.getVisibility() != View.GONE) { 1534 position += getIntrinsicHeight(child); 1535 if (i < getChildCount()-1) { 1536 position += mPaddingBetweenElements; 1537 } 1538 } 1539 } 1540 return 0; 1541 } 1542 1543 @Override 1544 protected void onViewAdded(View child) { 1545 super.onViewAdded(child); 1546 mStackScrollAlgorithm.notifyChildrenChanged(this); 1547 ((ExpandableView) child).setOnHeightChangedListener(this); 1548 generateAddAnimation(child, false /* fromMoreCard */); 1549 updateAnimationState(mAnimationsEnabled && mIsExpanded, child); 1550 } 1551 1552 public void setAnimationsEnabled(boolean animationsEnabled) { 1553 mAnimationsEnabled = animationsEnabled; 1554 updateNotificationAnimationStates(); 1555 } 1556 1557 private void updateNotificationAnimationStates() { 1558 boolean running = mIsExpanded && mAnimationsEnabled; 1559 int childCount = getChildCount(); 1560 for (int i = 0; i < childCount; i++) { 1561 View child = getChildAt(i); 1562 updateAnimationState(running, child); 1563 } 1564 } 1565 1566 private void updateAnimationState(boolean running, View child) { 1567 if (child instanceof ExpandableNotificationRow) { 1568 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 1569 row.setIconAnimationRunning(running); 1570 } 1571 } 1572 1573 public boolean isAddOrRemoveAnimationPending() { 1574 return mNeedsAnimation 1575 && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty()); 1576 } 1577 /** 1578 * Generate an animation for an added child view. 1579 * 1580 * @param child The view to be added. 1581 * @param fromMoreCard Whether this add is coming from the "more" card on lockscreen. 1582 */ 1583 public void generateAddAnimation(View child, boolean fromMoreCard) { 1584 if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) { 1585 // Generate Animations 1586 mChildrenToAddAnimated.add(child); 1587 if (fromMoreCard) { 1588 mFromMoreCardAdditions.add(child); 1589 } 1590 mNeedsAnimation = true; 1591 } 1592 } 1593 1594 /** 1595 * Change the position of child to a new location 1596 * 1597 * @param child the view to change the position for 1598 * @param newIndex the new index 1599 */ 1600 public void changeViewPosition(View child, int newIndex) { 1601 int currentIndex = indexOfChild(child); 1602 if (child != null && child.getParent() == this && currentIndex != newIndex) { 1603 mChangePositionInProgress = true; 1604 removeView(child); 1605 addView(child, newIndex); 1606 mChangePositionInProgress = false; 1607 if (mIsExpanded && mAnimationsEnabled && child.getVisibility() != View.GONE) { 1608 mChildrenChangingPositions.add(child); 1609 mNeedsAnimation = true; 1610 } 1611 } 1612 } 1613 1614 private void startAnimationToState() { 1615 if (mNeedsAnimation) { 1616 generateChildHierarchyEvents(); 1617 mNeedsAnimation = false; 1618 } 1619 if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) { 1620 mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState, 1621 mGoToFullShadeDelay); 1622 mAnimationEvents.clear(); 1623 } else { 1624 applyCurrentState(); 1625 } 1626 mGoToFullShadeDelay = 0; 1627 } 1628 1629 private void generateChildHierarchyEvents() { 1630 generateChildRemovalEvents(); 1631 generateChildAdditionEvents(); 1632 generatePositionChangeEvents(); 1633 generateSnapBackEvents(); 1634 generateDragEvents(); 1635 generateTopPaddingEvent(); 1636 generateActivateEvent(); 1637 generateDimmedEvent(); 1638 generateHideSensitiveEvent(); 1639 generateDarkEvent(); 1640 generateGoToFullShadeEvent(); 1641 generateViewResizeEvent(); 1642 generateAnimateEverythingEvent(); 1643 mNeedsAnimation = false; 1644 } 1645 1646 private void generateViewResizeEvent() { 1647 if (mNeedViewResizeAnimation) { 1648 mAnimationEvents.add( 1649 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_VIEW_RESIZE)); 1650 } 1651 mNeedViewResizeAnimation = false; 1652 } 1653 1654 private void generateSnapBackEvents() { 1655 for (View child : mSnappedBackChildren) { 1656 mAnimationEvents.add(new AnimationEvent(child, 1657 AnimationEvent.ANIMATION_TYPE_SNAP_BACK)); 1658 } 1659 mSnappedBackChildren.clear(); 1660 } 1661 1662 private void generateDragEvents() { 1663 for (View child : mDragAnimPendingChildren) { 1664 mAnimationEvents.add(new AnimationEvent(child, 1665 AnimationEvent.ANIMATION_TYPE_START_DRAG)); 1666 } 1667 mDragAnimPendingChildren.clear(); 1668 } 1669 1670 private void generateChildRemovalEvents() { 1671 for (View child : mChildrenToRemoveAnimated) { 1672 boolean childWasSwipedOut = mSwipedOutViews.contains(child); 1673 int animationType = childWasSwipedOut 1674 ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT 1675 : AnimationEvent.ANIMATION_TYPE_REMOVE; 1676 AnimationEvent event = new AnimationEvent(child, animationType); 1677 1678 // we need to know the view after this one 1679 event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY()); 1680 mAnimationEvents.add(event); 1681 } 1682 mSwipedOutViews.clear(); 1683 mChildrenToRemoveAnimated.clear(); 1684 } 1685 1686 private void generatePositionChangeEvents() { 1687 for (View child : mChildrenChangingPositions) { 1688 mAnimationEvents.add(new AnimationEvent(child, 1689 AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)); 1690 } 1691 mChildrenChangingPositions.clear(); 1692 } 1693 1694 private void generateChildAdditionEvents() { 1695 for (View child : mChildrenToAddAnimated) { 1696 if (mFromMoreCardAdditions.contains(child)) { 1697 mAnimationEvents.add(new AnimationEvent(child, 1698 AnimationEvent.ANIMATION_TYPE_ADD, 1699 StackStateAnimator.ANIMATION_DURATION_STANDARD)); 1700 } else { 1701 mAnimationEvents.add(new AnimationEvent(child, 1702 AnimationEvent.ANIMATION_TYPE_ADD)); 1703 } 1704 } 1705 mChildrenToAddAnimated.clear(); 1706 mFromMoreCardAdditions.clear(); 1707 } 1708 1709 private void generateTopPaddingEvent() { 1710 if (mTopPaddingNeedsAnimation) { 1711 mAnimationEvents.add( 1712 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED)); 1713 } 1714 mTopPaddingNeedsAnimation = false; 1715 } 1716 1717 private void generateActivateEvent() { 1718 if (mActivateNeedsAnimation) { 1719 mAnimationEvents.add( 1720 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD)); 1721 } 1722 mActivateNeedsAnimation = false; 1723 } 1724 1725 private void generateAnimateEverythingEvent() { 1726 if (mEverythingNeedsAnimation) { 1727 mAnimationEvents.add( 1728 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_EVERYTHING)); 1729 } 1730 mEverythingNeedsAnimation = false; 1731 } 1732 1733 private void generateDimmedEvent() { 1734 if (mDimmedNeedsAnimation) { 1735 mAnimationEvents.add( 1736 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED)); 1737 } 1738 mDimmedNeedsAnimation = false; 1739 } 1740 1741 private void generateHideSensitiveEvent() { 1742 if (mHideSensitiveNeedsAnimation) { 1743 mAnimationEvents.add( 1744 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_HIDE_SENSITIVE)); 1745 } 1746 mHideSensitiveNeedsAnimation = false; 1747 } 1748 1749 private void generateDarkEvent() { 1750 if (mDarkNeedsAnimation) { 1751 mAnimationEvents.add( 1752 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DARK)); 1753 } 1754 mDarkNeedsAnimation = false; 1755 } 1756 1757 private void generateGoToFullShadeEvent() { 1758 if (mGoToFullShadeNeedsAnimation) { 1759 mAnimationEvents.add( 1760 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE)); 1761 } 1762 mGoToFullShadeNeedsAnimation = false; 1763 } 1764 1765 private boolean onInterceptTouchEventScroll(MotionEvent ev) { 1766 if (!isScrollingEnabled()) { 1767 return false; 1768 } 1769 /* 1770 * This method JUST determines whether we want to intercept the motion. 1771 * If we return true, onMotionEvent will be called and we do the actual 1772 * scrolling there. 1773 */ 1774 1775 /* 1776 * Shortcut the most recurring case: the user is in the dragging 1777 * state and he is moving his finger. We want to intercept this 1778 * motion. 1779 */ 1780 final int action = ev.getAction(); 1781 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 1782 return true; 1783 } 1784 1785 switch (action & MotionEvent.ACTION_MASK) { 1786 case MotionEvent.ACTION_MOVE: { 1787 /* 1788 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1789 * whether the user has moved far enough from his original down touch. 1790 */ 1791 1792 /* 1793 * Locally do absolute value. mLastMotionY is set to the y value 1794 * of the down event. 1795 */ 1796 final int activePointerId = mActivePointerId; 1797 if (activePointerId == INVALID_POINTER) { 1798 // If we don't have a valid id, the touch down wasn't on content. 1799 break; 1800 } 1801 1802 final int pointerIndex = ev.findPointerIndex(activePointerId); 1803 if (pointerIndex == -1) { 1804 Log.e(TAG, "Invalid pointerId=" + activePointerId 1805 + " in onInterceptTouchEvent"); 1806 break; 1807 } 1808 1809 final int y = (int) ev.getY(pointerIndex); 1810 final int x = (int) ev.getX(pointerIndex); 1811 final int yDiff = Math.abs(y - mLastMotionY); 1812 final int xDiff = Math.abs(x - mDownX); 1813 if (yDiff > mTouchSlop && yDiff > xDiff) { 1814 setIsBeingDragged(true); 1815 mLastMotionY = y; 1816 mDownX = x; 1817 initVelocityTrackerIfNotExists(); 1818 mVelocityTracker.addMovement(ev); 1819 } 1820 break; 1821 } 1822 1823 case MotionEvent.ACTION_DOWN: { 1824 final int y = (int) ev.getY(); 1825 if (getChildAtPosition(ev.getX(), y) == null) { 1826 setIsBeingDragged(false); 1827 recycleVelocityTracker(); 1828 break; 1829 } 1830 1831 /* 1832 * Remember location of down touch. 1833 * ACTION_DOWN always refers to pointer index 0. 1834 */ 1835 mLastMotionY = y; 1836 mDownX = (int) ev.getX(); 1837 mActivePointerId = ev.getPointerId(0); 1838 mScrolledToTopOnFirstDown = isScrolledToTop(); 1839 1840 initOrResetVelocityTracker(); 1841 mVelocityTracker.addMovement(ev); 1842 /* 1843 * If being flinged and user touches the screen, initiate drag; 1844 * otherwise don't. mScroller.isFinished should be false when 1845 * being flinged. 1846 */ 1847 boolean isBeingDragged = !mScroller.isFinished(); 1848 setIsBeingDragged(isBeingDragged); 1849 break; 1850 } 1851 1852 case MotionEvent.ACTION_CANCEL: 1853 case MotionEvent.ACTION_UP: 1854 /* Release the drag */ 1855 setIsBeingDragged(false); 1856 mActivePointerId = INVALID_POINTER; 1857 recycleVelocityTracker(); 1858 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 1859 postInvalidateOnAnimation(); 1860 } 1861 break; 1862 case MotionEvent.ACTION_POINTER_UP: 1863 onSecondaryPointerUp(ev); 1864 break; 1865 } 1866 1867 /* 1868 * The only time we want to intercept motion events is if we are in the 1869 * drag mode. 1870 */ 1871 return mIsBeingDragged; 1872 } 1873 1874 /** 1875 * @return Whether the specified motion event is actually happening over the content. 1876 */ 1877 private boolean isInContentBounds(MotionEvent event) { 1878 return event.getY() < getHeight() - getEmptyBottomMargin(); 1879 } 1880 1881 private void setIsBeingDragged(boolean isDragged) { 1882 mIsBeingDragged = isDragged; 1883 if (isDragged) { 1884 requestDisallowInterceptTouchEvent(true); 1885 removeLongPressCallback(); 1886 } 1887 } 1888 1889 @Override 1890 public void onWindowFocusChanged(boolean hasWindowFocus) { 1891 super.onWindowFocusChanged(hasWindowFocus); 1892 if (!hasWindowFocus) { 1893 removeLongPressCallback(); 1894 } 1895 } 1896 1897 public void removeLongPressCallback() { 1898 mSwipeHelper.removeLongPressCallback(); 1899 } 1900 1901 @Override 1902 public boolean isScrolledToTop() { 1903 return mOwnScrollY == 0; 1904 } 1905 1906 @Override 1907 public boolean isScrolledToBottom() { 1908 return mOwnScrollY >= getScrollRange(); 1909 } 1910 1911 @Override 1912 public View getHostView() { 1913 return this; 1914 } 1915 1916 public int getEmptyBottomMargin() { 1917 int emptyMargin = mMaxLayoutHeight - mContentHeight - mBottomStackPeekSize; 1918 if (needsHeightAdaption()) { 1919 emptyMargin -= mBottomStackSlowDownHeight; 1920 } else { 1921 emptyMargin -= mCollapseSecondCardPadding; 1922 } 1923 return Math.max(emptyMargin, 0); 1924 } 1925 1926 public void onExpansionStarted() { 1927 mIsExpansionChanging = true; 1928 mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); 1929 } 1930 1931 public void onExpansionStopped() { 1932 mIsExpansionChanging = false; 1933 mStackScrollAlgorithm.onExpansionStopped(); 1934 if (!mIsExpanded) { 1935 mOwnScrollY = 0; 1936 } 1937 } 1938 1939 private void setIsExpanded(boolean isExpanded) { 1940 boolean changed = isExpanded != mIsExpanded; 1941 mIsExpanded = isExpanded; 1942 mStackScrollAlgorithm.setIsExpanded(isExpanded); 1943 if (changed) { 1944 updateNotificationAnimationStates(); 1945 } 1946 } 1947 1948 @Override 1949 public void onHeightChanged(ExpandableView view) { 1950 updateContentHeight(); 1951 updateScrollPositionOnExpandInBottom(view); 1952 clampScrollPosition(); 1953 notifyHeightChangeListener(view); 1954 requestChildrenUpdate(); 1955 } 1956 1957 @Override 1958 public void onReset(ExpandableView view) { 1959 if (mIsExpanded && mAnimationsEnabled) { 1960 mRequestViewResizeAnimationOnLayout = true; 1961 } 1962 mStackScrollAlgorithm.onReset(view); 1963 } 1964 1965 private void updateScrollPositionOnExpandInBottom(ExpandableView view) { 1966 if (view instanceof ExpandableNotificationRow) { 1967 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 1968 if (row.isUserLocked()) { 1969 // We are actually expanding this view 1970 float endPosition = row.getTranslationY() + row.getActualHeight(); 1971 int stackEnd = mMaxLayoutHeight - mBottomStackPeekSize - 1972 mBottomStackSlowDownHeight; 1973 if (endPosition > stackEnd) { 1974 mOwnScrollY += endPosition - stackEnd; 1975 mDisallowScrollingInThisMotion = true; 1976 } 1977 } 1978 } 1979 } 1980 1981 public void setOnHeightChangedListener( 1982 ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { 1983 this.mOnHeightChangedListener = mOnHeightChangedListener; 1984 } 1985 1986 public void onChildAnimationFinished() { 1987 requestChildrenUpdate(); 1988 } 1989 1990 /** 1991 * See {@link AmbientState#setDimmed}. 1992 */ 1993 public void setDimmed(boolean dimmed, boolean animate) { 1994 mStackScrollAlgorithm.setDimmed(dimmed); 1995 mAmbientState.setDimmed(dimmed); 1996 updatePadding(dimmed); 1997 if (animate && mAnimationsEnabled) { 1998 mDimmedNeedsAnimation = true; 1999 mNeedsAnimation = true; 2000 } 2001 requestChildrenUpdate(); 2002 } 2003 2004 public void setHideSensitive(boolean hideSensitive, boolean animate) { 2005 if (hideSensitive != mAmbientState.isHideSensitive()) { 2006 int childCount = getChildCount(); 2007 for (int i = 0; i < childCount; i++) { 2008 ExpandableView v = (ExpandableView) getChildAt(i); 2009 v.setHideSensitiveForIntrinsicHeight(hideSensitive); 2010 } 2011 mAmbientState.setHideSensitive(hideSensitive); 2012 if (animate && mAnimationsEnabled) { 2013 mHideSensitiveNeedsAnimation = true; 2014 mNeedsAnimation = true; 2015 } 2016 requestChildrenUpdate(); 2017 } 2018 } 2019 2020 /** 2021 * See {@link AmbientState#setActivatedChild}. 2022 */ 2023 public void setActivatedChild(ActivatableNotificationView activatedChild) { 2024 mAmbientState.setActivatedChild(activatedChild); 2025 if (mAnimationsEnabled) { 2026 mActivateNeedsAnimation = true; 2027 mNeedsAnimation = true; 2028 } 2029 requestChildrenUpdate(); 2030 } 2031 2032 public ActivatableNotificationView getActivatedChild() { 2033 return mAmbientState.getActivatedChild(); 2034 } 2035 2036 private void applyCurrentState() { 2037 mCurrentStackScrollState.apply(); 2038 if (mListener != null) { 2039 mListener.onChildLocationsChanged(this); 2040 } 2041 } 2042 2043 public void setSpeedBumpView(SpeedBumpView speedBumpView) { 2044 mSpeedBumpView = speedBumpView; 2045 addView(speedBumpView); 2046 } 2047 2048 private void updateSpeedBump(boolean visible) { 2049 boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE; 2050 if (visible != notGoneBefore) { 2051 int newVisibility = visible ? VISIBLE : GONE; 2052 mSpeedBumpView.setVisibility(newVisibility); 2053 if (visible) { 2054 // Make invisible to ensure that the appear animation is played. 2055 mSpeedBumpView.setInvisible(); 2056 } else { 2057 // TODO: This doesn't really work, because the view is already set to GONE above. 2058 generateRemoveAnimation(mSpeedBumpView); 2059 } 2060 } 2061 } 2062 2063 public void goToFullShade(long delay) { 2064 updateSpeedBump(true /* visibility */); 2065 mDismissView.setInvisible(); 2066 mEmptyShadeView.setInvisible(); 2067 mGoToFullShadeNeedsAnimation = true; 2068 mGoToFullShadeDelay = delay; 2069 mNeedsAnimation = true; 2070 requestChildrenUpdate(); 2071 } 2072 2073 public void cancelExpandHelper() { 2074 mExpandHelper.cancel(); 2075 } 2076 2077 public void setIntrinsicPadding(int intrinsicPadding) { 2078 mIntrinsicPadding = intrinsicPadding; 2079 } 2080 2081 public int getIntrinsicPadding() { 2082 return mIntrinsicPadding; 2083 } 2084 2085 /** 2086 * @return the y position of the first notification 2087 */ 2088 public float getNotificationsTopY() { 2089 return mTopPadding + getTranslationY(); 2090 } 2091 2092 @Override 2093 public boolean shouldDelayChildPressedState() { 2094 return true; 2095 } 2096 2097 /** 2098 * See {@link AmbientState#setDark}. 2099 */ 2100 public void setDark(boolean dark, boolean animate) { 2101 mAmbientState.setDark(dark); 2102 if (animate && mAnimationsEnabled) { 2103 mDarkNeedsAnimation = true; 2104 mNeedsAnimation = true; 2105 } 2106 requestChildrenUpdate(); 2107 } 2108 2109 public void setDismissView(DismissView dismissView) { 2110 mDismissView = dismissView; 2111 addView(mDismissView); 2112 } 2113 2114 public void setEmptyShadeView(EmptyShadeView emptyShadeView) { 2115 mEmptyShadeView = emptyShadeView; 2116 addView(mEmptyShadeView); 2117 } 2118 2119 public void updateEmptyShadeView(boolean visible) { 2120 int oldVisibility = mEmptyShadeView.willBeGone() ? GONE : mEmptyShadeView.getVisibility(); 2121 int newVisibility = visible ? VISIBLE : GONE; 2122 if (oldVisibility != newVisibility) { 2123 if (newVisibility != GONE) { 2124 if (mEmptyShadeView.willBeGone()) { 2125 mEmptyShadeView.cancelAnimation(); 2126 } else { 2127 mEmptyShadeView.setInvisible(); 2128 } 2129 mEmptyShadeView.setVisibility(newVisibility); 2130 mEmptyShadeView.setWillBeGone(false); 2131 updateContentHeight(); 2132 notifyHeightChangeListener(mDismissView); 2133 } else { 2134 mEmptyShadeView.setWillBeGone(true); 2135 mEmptyShadeView.performVisibilityAnimation(false, new Runnable() { 2136 @Override 2137 public void run() { 2138 mEmptyShadeView.setVisibility(GONE); 2139 mEmptyShadeView.setWillBeGone(false); 2140 updateContentHeight(); 2141 notifyHeightChangeListener(mDismissView); 2142 } 2143 }); 2144 } 2145 } 2146 } 2147 2148 public void updateDismissView(boolean visible) { 2149 int oldVisibility = mDismissView.willBeGone() ? GONE : mDismissView.getVisibility(); 2150 int newVisibility = visible ? VISIBLE : GONE; 2151 if (oldVisibility != newVisibility) { 2152 if (newVisibility != GONE) { 2153 if (mDismissView.willBeGone()) { 2154 mDismissView.cancelAnimation(); 2155 } else { 2156 mDismissView.setInvisible(); 2157 } 2158 mDismissView.setVisibility(newVisibility); 2159 mDismissView.setWillBeGone(false); 2160 updateContentHeight(); 2161 notifyHeightChangeListener(mDismissView); 2162 } else { 2163 mDismissView.setWillBeGone(true); 2164 mDismissView.performVisibilityAnimation(false, new Runnable() { 2165 @Override 2166 public void run() { 2167 mDismissView.setVisibility(GONE); 2168 mDismissView.setWillBeGone(false); 2169 updateContentHeight(); 2170 notifyHeightChangeListener(mDismissView); 2171 } 2172 }); 2173 } 2174 } 2175 } 2176 2177 public void setDismissAllInProgress(boolean dismissAllInProgress) { 2178 mDismissAllInProgress = dismissAllInProgress; 2179 } 2180 2181 public boolean isDismissViewNotGone() { 2182 return mDismissView.getVisibility() != View.GONE && !mDismissView.willBeGone(); 2183 } 2184 2185 public boolean isDismissViewVisible() { 2186 return mDismissView.isVisible(); 2187 } 2188 2189 public int getDismissViewHeight() { 2190 int height = mDismissView.getHeight() + mPaddingBetweenElementsNormal; 2191 2192 // Hack: Accommodate for additional distance when we only have one notification and the 2193 // dismiss all button. 2194 if (getNotGoneChildCount() == 2 && getLastChildNotGone() == mDismissView 2195 && getFirstChildNotGone() instanceof ActivatableNotificationView) { 2196 height += mCollapseSecondCardPadding; 2197 } 2198 return height; 2199 } 2200 2201 public float getBottomMostNotificationBottom() { 2202 final int count = getChildCount(); 2203 float max = 0; 2204 for (int childIdx = 0; childIdx < count; childIdx++) { 2205 ExpandableView child = (ExpandableView) getChildAt(childIdx); 2206 if (child.getVisibility() == GONE) { 2207 continue; 2208 } 2209 float bottom = child.getTranslationY() + child.getActualHeight(); 2210 if (bottom > max) { 2211 max = bottom; 2212 } 2213 } 2214 return max + getTranslationY(); 2215 } 2216 2217 /** 2218 * @param qsMinHeight The minimum height of the quick settings including padding 2219 * See {@link StackScrollAlgorithm#updateIsSmallScreen}. 2220 */ 2221 public void updateIsSmallScreen(int qsMinHeight) { 2222 mStackScrollAlgorithm.updateIsSmallScreen(mMaxLayoutHeight - qsMinHeight); 2223 } 2224 2225 public void setPhoneStatusBar(PhoneStatusBar phoneStatusBar) { 2226 this.mPhoneStatusBar = phoneStatusBar; 2227 } 2228 2229 public void onGoToKeyguard() { 2230 if (mIsExpanded && mAnimationsEnabled) { 2231 mEverythingNeedsAnimation = true; 2232 requestChildrenUpdate(); 2233 } 2234 } 2235 2236 /** 2237 * A listener that is notified when some child locations might have changed. 2238 */ 2239 public interface OnChildLocationsChangedListener { 2240 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); 2241 } 2242 2243 /** 2244 * A listener that gets notified when the overscroll at the top has changed. 2245 */ 2246 public interface OnOverscrollTopChangedListener { 2247 2248 /** 2249 * Notifies a listener that the overscroll has changed. 2250 * 2251 * @param amount the amount of overscroll, in pixels 2252 * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an 2253 * unrubberbanded motion to directly expand overscroll view (e.g expand 2254 * QS) 2255 */ 2256 public void onOverscrollTopChanged(float amount, boolean isRubberbanded); 2257 2258 /** 2259 * Notify a listener that the scroller wants to escape from the scrolling motion and 2260 * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS) 2261 * 2262 * @param velocity The velocity that the Scroller had when over flinging 2263 * @param open Should the fling open or close the overscroll view. 2264 */ 2265 public void flingTopOverscroll(float velocity, boolean open); 2266 } 2267 2268 static class AnimationEvent { 2269 2270 static AnimationFilter[] FILTERS = new AnimationFilter[] { 2271 2272 // ANIMATION_TYPE_ADD 2273 new AnimationFilter() 2274 .animateAlpha() 2275 .animateHeight() 2276 .animateTopInset() 2277 .animateY() 2278 .animateZ() 2279 .hasDelays(), 2280 2281 // ANIMATION_TYPE_REMOVE 2282 new AnimationFilter() 2283 .animateAlpha() 2284 .animateHeight() 2285 .animateTopInset() 2286 .animateY() 2287 .animateZ() 2288 .hasDelays(), 2289 2290 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 2291 new AnimationFilter() 2292 .animateAlpha() 2293 .animateHeight() 2294 .animateTopInset() 2295 .animateY() 2296 .animateZ() 2297 .hasDelays(), 2298 2299 // ANIMATION_TYPE_TOP_PADDING_CHANGED 2300 new AnimationFilter() 2301 .animateAlpha() 2302 .animateHeight() 2303 .animateTopInset() 2304 .animateY() 2305 .animateDimmed() 2306 .animateScale() 2307 .animateZ(), 2308 2309 // ANIMATION_TYPE_START_DRAG 2310 new AnimationFilter() 2311 .animateAlpha(), 2312 2313 // ANIMATION_TYPE_SNAP_BACK 2314 new AnimationFilter() 2315 .animateAlpha() 2316 .animateHeight(), 2317 2318 // ANIMATION_TYPE_ACTIVATED_CHILD 2319 new AnimationFilter() 2320 .animateScale() 2321 .animateAlpha(), 2322 2323 // ANIMATION_TYPE_DIMMED 2324 new AnimationFilter() 2325 .animateY() 2326 .animateScale() 2327 .animateDimmed(), 2328 2329 // ANIMATION_TYPE_CHANGE_POSITION 2330 new AnimationFilter() 2331 .animateAlpha() 2332 .animateHeight() 2333 .animateTopInset() 2334 .animateY() 2335 .animateZ(), 2336 2337 // ANIMATION_TYPE_DARK 2338 new AnimationFilter() 2339 .animateDark(), 2340 2341 // ANIMATION_TYPE_GO_TO_FULL_SHADE 2342 new AnimationFilter() 2343 .animateAlpha() 2344 .animateHeight() 2345 .animateTopInset() 2346 .animateY() 2347 .animateDimmed() 2348 .animateScale() 2349 .animateZ() 2350 .hasDelays(), 2351 2352 // ANIMATION_TYPE_HIDE_SENSITIVE 2353 new AnimationFilter() 2354 .animateHideSensitive(), 2355 2356 // ANIMATION_TYPE_VIEW_RESIZE 2357 new AnimationFilter() 2358 .animateAlpha() 2359 .animateHeight() 2360 .animateTopInset() 2361 .animateY() 2362 .animateZ(), 2363 2364 // ANIMATION_TYPE_EVERYTHING 2365 new AnimationFilter() 2366 .animateAlpha() 2367 .animateDark() 2368 .animateScale() 2369 .animateDimmed() 2370 .animateHideSensitive() 2371 .animateHeight() 2372 .animateTopInset() 2373 .animateY() 2374 .animateZ(), 2375 }; 2376 2377 static int[] LENGTHS = new int[] { 2378 2379 // ANIMATION_TYPE_ADD 2380 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 2381 2382 // ANIMATION_TYPE_REMOVE 2383 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 2384 2385 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 2386 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2387 2388 // ANIMATION_TYPE_TOP_PADDING_CHANGED 2389 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2390 2391 // ANIMATION_TYPE_START_DRAG 2392 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2393 2394 // ANIMATION_TYPE_SNAP_BACK 2395 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2396 2397 // ANIMATION_TYPE_ACTIVATED_CHILD 2398 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 2399 2400 // ANIMATION_TYPE_DIMMED 2401 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 2402 2403 // ANIMATION_TYPE_CHANGE_POSITION 2404 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2405 2406 // ANIMATION_TYPE_DARK 2407 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2408 2409 // ANIMATION_TYPE_GO_TO_FULL_SHADE 2410 StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, 2411 2412 // ANIMATION_TYPE_HIDE_SENSITIVE 2413 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2414 2415 // ANIMATION_TYPE_VIEW_RESIZE 2416 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2417 2418 // ANIMATION_TYPE_EVERYTHING 2419 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2420 }; 2421 2422 static final int ANIMATION_TYPE_ADD = 0; 2423 static final int ANIMATION_TYPE_REMOVE = 1; 2424 static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2; 2425 static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3; 2426 static final int ANIMATION_TYPE_START_DRAG = 4; 2427 static final int ANIMATION_TYPE_SNAP_BACK = 5; 2428 static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6; 2429 static final int ANIMATION_TYPE_DIMMED = 7; 2430 static final int ANIMATION_TYPE_CHANGE_POSITION = 8; 2431 static final int ANIMATION_TYPE_DARK = 9; 2432 static final int ANIMATION_TYPE_GO_TO_FULL_SHADE = 10; 2433 static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11; 2434 static final int ANIMATION_TYPE_VIEW_RESIZE = 12; 2435 static final int ANIMATION_TYPE_EVERYTHING = 13; 2436 2437 final long eventStartTime; 2438 final View changingView; 2439 final int animationType; 2440 final AnimationFilter filter; 2441 final long length; 2442 View viewAfterChangingView; 2443 2444 AnimationEvent(View view, int type) { 2445 this(view, type, LENGTHS[type]); 2446 } 2447 2448 AnimationEvent(View view, int type, long length) { 2449 eventStartTime = AnimationUtils.currentAnimationTimeMillis(); 2450 changingView = view; 2451 animationType = type; 2452 filter = FILTERS[type]; 2453 this.length = length; 2454 } 2455 2456 /** 2457 * Combines the length of several animation events into a single value. 2458 * 2459 * @param events The events of the lengths to combine. 2460 * @return The combined length. Depending on the event types, this might be the maximum of 2461 * all events or the length of a specific event. 2462 */ 2463 static long combineLength(ArrayList<AnimationEvent> events) { 2464 long length = 0; 2465 int size = events.size(); 2466 for (int i = 0; i < size; i++) { 2467 AnimationEvent event = events.get(i); 2468 length = Math.max(length, event.length); 2469 if (event.animationType == ANIMATION_TYPE_GO_TO_FULL_SHADE) { 2470 return event.length; 2471 } 2472 } 2473 return length; 2474 } 2475 } 2476 2477} 2478