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