NotificationStackScrollLayout.java revision a2052ea218386877e6d5d2136483a62b2b31f774
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 int left = slidingChild.getLeft(); 572 int right = slidingChild.getRight(); 573 574 if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) { 575 return slidingChild; 576 } 577 } 578 return null; 579 } 580 581 public boolean canChildBeExpanded(View v) { 582 return v instanceof ExpandableNotificationRow 583 && ((ExpandableNotificationRow) v).isExpandable(); 584 } 585 586 public void setUserExpandedChild(View v, boolean userExpanded) { 587 if (v instanceof ExpandableNotificationRow) { 588 ((ExpandableNotificationRow) v).setUserExpanded(userExpanded); 589 } 590 } 591 592 public void setUserLockedChild(View v, boolean userLocked) { 593 if (v instanceof ExpandableNotificationRow) { 594 ((ExpandableNotificationRow) v).setUserLocked(userLocked); 595 } 596 removeLongPressCallback(); 597 requestDisallowInterceptTouchEvent(true); 598 } 599 600 @Override 601 public void expansionStateChanged(boolean isExpanding) { 602 mExpandingNotification = isExpanding; 603 if (!mExpandedInThisMotion) { 604 mMaxScrollAfterExpand = mOwnScrollY; 605 mExpandedInThisMotion = true; 606 } 607 } 608 609 public void setScrollingEnabled(boolean enable) { 610 mScrollingEnabled = enable; 611 } 612 613 public void setExpandingEnabled(boolean enable) { 614 mExpandHelper.setEnabled(enable); 615 } 616 617 private boolean isScrollingEnabled() { 618 return mScrollingEnabled; 619 } 620 621 public View getChildContentView(View v) { 622 return v; 623 } 624 625 public boolean canChildBeDismissed(View v) { 626 final View veto = v.findViewById(R.id.veto); 627 return (veto != null && veto.getVisibility() != View.GONE); 628 } 629 630 private void setSwipingInProgress(boolean isSwiped) { 631 mSwipingInProgress = isSwiped; 632 if(isSwiped) { 633 requestDisallowInterceptTouchEvent(true); 634 } 635 } 636 637 @Override 638 protected void onConfigurationChanged(Configuration newConfig) { 639 super.onConfigurationChanged(newConfig); 640 float densityScale = getResources().getDisplayMetrics().density; 641 mSwipeHelper.setDensityScale(densityScale); 642 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 643 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 644 initView(getContext()); 645 } 646 647 public void dismissViewAnimated(View child, Runnable endRunnable, int delay, long duration) { 648 child.setClipBounds(null); 649 mSwipeHelper.dismissChild(child, 0, endRunnable, delay, true, duration); 650 } 651 652 @Override 653 public boolean onTouchEvent(MotionEvent ev) { 654 boolean isCancelOrUp = ev.getActionMasked() == MotionEvent.ACTION_CANCEL 655 || ev.getActionMasked()== MotionEvent.ACTION_UP; 656 if (mDelegateToScrollView) { 657 if (isCancelOrUp) { 658 mDelegateToScrollView = false; 659 } 660 transformTouchEvent(ev, this, mScrollView); 661 return mScrollView.onTouchEvent(ev); 662 } 663 boolean expandWantsIt = false; 664 if (!mSwipingInProgress && !mOnlyScrollingInThisMotion && isScrollingEnabled()) { 665 if (isCancelOrUp) { 666 mExpandHelper.onlyObserveMovements(false); 667 } 668 boolean wasExpandingBefore = mExpandingNotification; 669 expandWantsIt = mExpandHelper.onTouchEvent(ev); 670 if (mExpandedInThisMotion && !mExpandingNotification && wasExpandingBefore 671 && !mDisallowScrollingInThisMotion) { 672 dispatchDownEventToScroller(ev); 673 } 674 } 675 boolean scrollerWantsIt = false; 676 if (!mSwipingInProgress && !mExpandingNotification && !mDisallowScrollingInThisMotion) { 677 scrollerWantsIt = onScrollTouch(ev); 678 } 679 boolean horizontalSwipeWantsIt = false; 680 if (!mIsBeingDragged 681 && !mExpandingNotification 682 && !mExpandedInThisMotion 683 && !mOnlyScrollingInThisMotion) { 684 horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev); 685 } 686 return horizontalSwipeWantsIt || scrollerWantsIt || expandWantsIt || super.onTouchEvent(ev); 687 } 688 689 private void dispatchDownEventToScroller(MotionEvent ev) { 690 MotionEvent downEvent = MotionEvent.obtain(ev); 691 downEvent.setAction(MotionEvent.ACTION_DOWN); 692 onScrollTouch(downEvent); 693 downEvent.recycle(); 694 } 695 696 private boolean onScrollTouch(MotionEvent ev) { 697 if (!isScrollingEnabled()) { 698 return false; 699 } 700 initVelocityTrackerIfNotExists(); 701 mVelocityTracker.addMovement(ev); 702 703 final int action = ev.getAction(); 704 705 switch (action & MotionEvent.ACTION_MASK) { 706 case MotionEvent.ACTION_DOWN: { 707 if (getChildCount() == 0 || !isInContentBounds(ev)) { 708 return false; 709 } 710 boolean isBeingDragged = !mScroller.isFinished(); 711 setIsBeingDragged(isBeingDragged); 712 713 /* 714 * If being flinged and user touches, stop the fling. isFinished 715 * will be false if being flinged. 716 */ 717 if (!mScroller.isFinished()) { 718 mScroller.forceFinished(true); 719 } 720 721 // Remember where the motion event started 722 mLastMotionY = (int) ev.getY(); 723 mDownX = (int) ev.getX(); 724 mActivePointerId = ev.getPointerId(0); 725 break; 726 } 727 case MotionEvent.ACTION_MOVE: 728 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 729 if (activePointerIndex == -1) { 730 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 731 break; 732 } 733 734 final int y = (int) ev.getY(activePointerIndex); 735 final int x = (int) ev.getX(activePointerIndex); 736 int deltaY = mLastMotionY - y; 737 final int xDiff = Math.abs(x - mDownX); 738 final int yDiff = Math.abs(deltaY); 739 if (!mIsBeingDragged && yDiff > mTouchSlop && yDiff > xDiff) { 740 setIsBeingDragged(true); 741 if (deltaY > 0) { 742 deltaY -= mTouchSlop; 743 } else { 744 deltaY += mTouchSlop; 745 } 746 } 747 if (mIsBeingDragged) { 748 // Scroll to follow the motion event 749 mLastMotionY = y; 750 int range = getScrollRange(); 751 if (mExpandedInThisMotion) { 752 range = Math.min(range, mMaxScrollAfterExpand); 753 } 754 755 float scrollAmount; 756 if (deltaY < 0) { 757 scrollAmount = overScrollDown(deltaY); 758 } else { 759 scrollAmount = overScrollUp(deltaY, range); 760 } 761 762 // Calling overScrollBy will call onOverScrolled, which 763 // calls onScrollChanged if applicable. 764 if (scrollAmount != 0.0f) { 765 // The scrolling motion could not be compensated with the 766 // existing overScroll, we have to scroll the view 767 overScrollBy(0, (int) scrollAmount, 0, mOwnScrollY, 768 0, range, 0, getHeight() / 2, true); 769 } 770 } 771 break; 772 case MotionEvent.ACTION_UP: 773 if (mIsBeingDragged) { 774 final VelocityTracker velocityTracker = mVelocityTracker; 775 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 776 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 777 778 if (shouldOverScrollFling(initialVelocity)) { 779 onOverScrollFling(true, initialVelocity); 780 } else { 781 if (getChildCount() > 0) { 782 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 783 float currentOverScrollTop = getCurrentOverScrollAmount(true); 784 if (currentOverScrollTop == 0.0f || initialVelocity > 0) { 785 fling(-initialVelocity); 786 } else { 787 onOverScrollFling(false, initialVelocity); 788 } 789 } else { 790 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, 791 getScrollRange())) { 792 postInvalidateOnAnimation(); 793 } 794 } 795 } 796 } 797 798 mActivePointerId = INVALID_POINTER; 799 endDrag(); 800 } 801 802 break; 803 case MotionEvent.ACTION_CANCEL: 804 if (mIsBeingDragged && getChildCount() > 0) { 805 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 806 postInvalidateOnAnimation(); 807 } 808 mActivePointerId = INVALID_POINTER; 809 endDrag(); 810 } 811 break; 812 case MotionEvent.ACTION_POINTER_DOWN: { 813 final int index = ev.getActionIndex(); 814 mLastMotionY = (int) ev.getY(index); 815 mDownX = (int) ev.getX(index); 816 mActivePointerId = ev.getPointerId(index); 817 break; 818 } 819 case MotionEvent.ACTION_POINTER_UP: 820 onSecondaryPointerUp(ev); 821 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 822 mDownX = (int) ev.getX(ev.findPointerIndex(mActivePointerId)); 823 break; 824 } 825 return true; 826 } 827 828 private void onOverScrollFling(boolean open, int initialVelocity) { 829 if (mOverscrollTopChangedListener != null) { 830 mOverscrollTopChangedListener.flingTopOverscroll(initialVelocity, open); 831 } 832 mDontReportNextOverScroll = true; 833 setOverScrollAmount(0.0f, true, false); 834 } 835 836 /** 837 * Perform a scroll upwards and adapt the overscroll amounts accordingly 838 * 839 * @param deltaY The amount to scroll upwards, has to be positive. 840 * @return The amount of scrolling to be performed by the scroller, 841 * not handled by the overScroll amount. 842 */ 843 private float overScrollUp(int deltaY, int range) { 844 deltaY = Math.max(deltaY, 0); 845 float currentTopAmount = getCurrentOverScrollAmount(true); 846 float newTopAmount = currentTopAmount - deltaY; 847 if (currentTopAmount > 0) { 848 setOverScrollAmount(newTopAmount, true /* onTop */, 849 false /* animate */); 850 } 851 // Top overScroll might not grab all scrolling motion, 852 // we have to scroll as well. 853 float scrollAmount = newTopAmount < 0 ? -newTopAmount : 0.0f; 854 float newScrollY = mOwnScrollY + scrollAmount; 855 if (newScrollY > range) { 856 if (!mExpandedInThisMotion) { 857 float currentBottomPixels = getCurrentOverScrolledPixels(false); 858 // We overScroll on the top 859 setOverScrolledPixels(currentBottomPixels + newScrollY - range, 860 false /* onTop */, 861 false /* animate */); 862 } 863 mOwnScrollY = range; 864 scrollAmount = 0.0f; 865 } 866 return scrollAmount; 867 } 868 869 /** 870 * Perform a scroll downward and adapt the overscroll amounts accordingly 871 * 872 * @param deltaY The amount to scroll downwards, has to be negative. 873 * @return The amount of scrolling to be performed by the scroller, 874 * not handled by the overScroll amount. 875 */ 876 private float overScrollDown(int deltaY) { 877 deltaY = Math.min(deltaY, 0); 878 float currentBottomAmount = getCurrentOverScrollAmount(false); 879 float newBottomAmount = currentBottomAmount + deltaY; 880 if (currentBottomAmount > 0) { 881 setOverScrollAmount(newBottomAmount, false /* onTop */, 882 false /* animate */); 883 } 884 // Bottom overScroll might not grab all scrolling motion, 885 // we have to scroll as well. 886 float scrollAmount = newBottomAmount < 0 ? newBottomAmount : 0.0f; 887 float newScrollY = mOwnScrollY + scrollAmount; 888 if (newScrollY < 0) { 889 float currentTopPixels = getCurrentOverScrolledPixels(true); 890 // We overScroll on the top 891 setOverScrolledPixels(currentTopPixels - newScrollY, 892 true /* onTop */, 893 false /* animate */); 894 mOwnScrollY = 0; 895 scrollAmount = 0.0f; 896 } 897 return scrollAmount; 898 } 899 900 private void onSecondaryPointerUp(MotionEvent ev) { 901 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 902 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 903 final int pointerId = ev.getPointerId(pointerIndex); 904 if (pointerId == mActivePointerId) { 905 // This was our active pointer going up. Choose a new 906 // active pointer and adjust accordingly. 907 // TODO: Make this decision more intelligent. 908 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 909 mLastMotionY = (int) ev.getY(newPointerIndex); 910 mActivePointerId = ev.getPointerId(newPointerIndex); 911 if (mVelocityTracker != null) { 912 mVelocityTracker.clear(); 913 } 914 } 915 } 916 917 private void initVelocityTrackerIfNotExists() { 918 if (mVelocityTracker == null) { 919 mVelocityTracker = VelocityTracker.obtain(); 920 } 921 } 922 923 private void recycleVelocityTracker() { 924 if (mVelocityTracker != null) { 925 mVelocityTracker.recycle(); 926 mVelocityTracker = null; 927 } 928 } 929 930 private void initOrResetVelocityTracker() { 931 if (mVelocityTracker == null) { 932 mVelocityTracker = VelocityTracker.obtain(); 933 } else { 934 mVelocityTracker.clear(); 935 } 936 } 937 938 @Override 939 public void computeScroll() { 940 if (mScroller.computeScrollOffset()) { 941 // This is called at drawing time by ViewGroup. 942 int oldX = mScrollX; 943 int oldY = mOwnScrollY; 944 int x = mScroller.getCurrX(); 945 int y = mScroller.getCurrY(); 946 947 if (oldX != x || oldY != y) { 948 final int range = getScrollRange(); 949 if (y < 0 && oldY >= 0 || y > range && oldY <= range) { 950 float currVelocity = mScroller.getCurrVelocity(); 951 if (currVelocity >= mMinimumVelocity) { 952 mMaxOverScroll = Math.abs(currVelocity) / 1000 * mOverflingDistance; 953 } 954 } 955 956 overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range, 957 0, (int) (mMaxOverScroll), false); 958 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 959 } 960 961 // Keep on drawing until the animation has finished. 962 postInvalidateOnAnimation(); 963 } 964 } 965 966 @Override 967 protected boolean overScrollBy(int deltaX, int deltaY, 968 int scrollX, int scrollY, 969 int scrollRangeX, int scrollRangeY, 970 int maxOverScrollX, int maxOverScrollY, 971 boolean isTouchEvent) { 972 973 int newScrollY = scrollY + deltaY; 974 975 final int top = -maxOverScrollY; 976 final int bottom = maxOverScrollY + scrollRangeY; 977 978 boolean clampedY = false; 979 if (newScrollY > bottom) { 980 newScrollY = bottom; 981 clampedY = true; 982 } else if (newScrollY < top) { 983 newScrollY = top; 984 clampedY = true; 985 } 986 987 onOverScrolled(0, newScrollY, false, clampedY); 988 989 return clampedY; 990 } 991 992 /** 993 * Set the amount of overScrolled pixels which will force the view to apply a rubber-banded 994 * overscroll effect based on numPixels. By default this will also cancel animations on the 995 * same overScroll edge. 996 * 997 * @param numPixels The amount of pixels to overScroll by. These will be scaled according to 998 * the rubber-banding logic. 999 * @param onTop Should the effect be applied on top of the scroller. 1000 * @param animate Should an animation be performed. 1001 */ 1002 public void setOverScrolledPixels(float numPixels, boolean onTop, boolean animate) { 1003 setOverScrollAmount(numPixels * getRubberBandFactor(onTop), onTop, animate, true); 1004 } 1005 1006 /** 1007 * Set the effective overScroll amount which will be directly reflected in the layout. 1008 * By default this will also cancel animations on the same overScroll edge. 1009 * 1010 * @param amount The amount to overScroll by. 1011 * @param onTop Should the effect be applied on top of the scroller. 1012 * @param animate Should an animation be performed. 1013 */ 1014 public void setOverScrollAmount(float amount, boolean onTop, boolean animate) { 1015 setOverScrollAmount(amount, onTop, animate, true); 1016 } 1017 1018 /** 1019 * Set the effective overScroll amount which will be directly reflected in the layout. 1020 * 1021 * @param amount The amount to overScroll by. 1022 * @param onTop Should the effect be applied on top of the scroller. 1023 * @param animate Should an animation be performed. 1024 * @param cancelAnimators Should running animations be cancelled. 1025 */ 1026 public void setOverScrollAmount(float amount, boolean onTop, boolean animate, 1027 boolean cancelAnimators) { 1028 setOverScrollAmount(amount, onTop, animate, cancelAnimators, isRubberbanded(onTop)); 1029 } 1030 1031 /** 1032 * Set the effective overScroll amount which will be directly reflected in the layout. 1033 * 1034 * @param amount The amount to overScroll by. 1035 * @param onTop Should the effect be applied on top of the scroller. 1036 * @param animate Should an animation be performed. 1037 * @param cancelAnimators Should running animations be cancelled. 1038 * @param isRubberbanded The value which will be passed to 1039 * {@link OnOverscrollTopChangedListener#onOverscrollTopChanged} 1040 */ 1041 public void setOverScrollAmount(float amount, boolean onTop, boolean animate, 1042 boolean cancelAnimators, boolean isRubberbanded) { 1043 if (cancelAnimators) { 1044 mStateAnimator.cancelOverScrollAnimators(onTop); 1045 } 1046 setOverScrollAmountInternal(amount, onTop, animate, isRubberbanded); 1047 } 1048 1049 private void setOverScrollAmountInternal(float amount, boolean onTop, boolean animate, 1050 boolean isRubberbanded) { 1051 amount = Math.max(0, amount); 1052 if (animate) { 1053 mStateAnimator.animateOverScrollToAmount(amount, onTop, isRubberbanded); 1054 } else { 1055 setOverScrolledPixels(amount / getRubberBandFactor(onTop), onTop); 1056 mAmbientState.setOverScrollAmount(amount, onTop); 1057 if (onTop) { 1058 notifyOverscrollTopListener(amount, isRubberbanded); 1059 } 1060 requestChildrenUpdate(); 1061 } 1062 } 1063 1064 private void notifyOverscrollTopListener(float amount, boolean isRubberbanded) { 1065 mExpandHelper.onlyObserveMovements(amount > 1.0f); 1066 if (mDontReportNextOverScroll) { 1067 mDontReportNextOverScroll = false; 1068 return; 1069 } 1070 if (mOverscrollTopChangedListener != null) { 1071 mOverscrollTopChangedListener.onOverscrollTopChanged(amount, isRubberbanded); 1072 } 1073 } 1074 1075 public void setOverscrollTopChangedListener( 1076 OnOverscrollTopChangedListener overscrollTopChangedListener) { 1077 mOverscrollTopChangedListener = overscrollTopChangedListener; 1078 } 1079 1080 public float getCurrentOverScrollAmount(boolean top) { 1081 return mAmbientState.getOverScrollAmount(top); 1082 } 1083 1084 public float getCurrentOverScrolledPixels(boolean top) { 1085 return top? mOverScrolledTopPixels : mOverScrolledBottomPixels; 1086 } 1087 1088 private void setOverScrolledPixels(float amount, boolean onTop) { 1089 if (onTop) { 1090 mOverScrolledTopPixels = amount; 1091 } else { 1092 mOverScrolledBottomPixels = amount; 1093 } 1094 } 1095 1096 private void customScrollTo(int y) { 1097 mOwnScrollY = y; 1098 updateChildren(); 1099 } 1100 1101 @Override 1102 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 1103 // Treat animating scrolls differently; see #computeScroll() for why. 1104 if (!mScroller.isFinished()) { 1105 final int oldX = mScrollX; 1106 final int oldY = mOwnScrollY; 1107 mScrollX = scrollX; 1108 mOwnScrollY = scrollY; 1109 if (clampedY) { 1110 springBack(); 1111 } else { 1112 onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY); 1113 invalidateParentIfNeeded(); 1114 updateChildren(); 1115 float overScrollTop = getCurrentOverScrollAmount(true); 1116 if (mOwnScrollY < 0) { 1117 notifyOverscrollTopListener(-mOwnScrollY, isRubberbanded(true)); 1118 } else { 1119 notifyOverscrollTopListener(overScrollTop, isRubberbanded(true)); 1120 } 1121 } 1122 } else { 1123 customScrollTo(scrollY); 1124 scrollTo(scrollX, mScrollY); 1125 } 1126 } 1127 1128 private void springBack() { 1129 int scrollRange = getScrollRange(); 1130 boolean overScrolledTop = mOwnScrollY <= 0; 1131 boolean overScrolledBottom = mOwnScrollY >= scrollRange; 1132 if (overScrolledTop || overScrolledBottom) { 1133 boolean onTop; 1134 float newAmount; 1135 if (overScrolledTop) { 1136 onTop = true; 1137 newAmount = -mOwnScrollY; 1138 mOwnScrollY = 0; 1139 mDontReportNextOverScroll = true; 1140 } else { 1141 onTop = false; 1142 newAmount = mOwnScrollY - scrollRange; 1143 mOwnScrollY = scrollRange; 1144 } 1145 setOverScrollAmount(newAmount, onTop, false); 1146 setOverScrollAmount(0.0f, onTop, true); 1147 mScroller.forceFinished(true); 1148 } 1149 } 1150 1151 private int getScrollRange() { 1152 int scrollRange = 0; 1153 ExpandableView firstChild = (ExpandableView) getFirstChildNotGone(); 1154 if (firstChild != null) { 1155 int contentHeight = getContentHeight(); 1156 int firstChildMaxExpandHeight = getMaxExpandHeight(firstChild); 1157 scrollRange = Math.max(0, contentHeight - mMaxLayoutHeight + mBottomStackPeekSize 1158 + mBottomStackSlowDownHeight); 1159 if (scrollRange > 0) { 1160 View lastChild = getLastChildNotGone(); 1161 // We want to at least be able collapse the first item and not ending in a weird 1162 // end state. 1163 scrollRange = Math.max(scrollRange, firstChildMaxExpandHeight - mCollapsedSize); 1164 } 1165 } 1166 return scrollRange; 1167 } 1168 1169 /** 1170 * @return the first child which has visibility unequal to GONE 1171 */ 1172 private View getFirstChildNotGone() { 1173 int childCount = getChildCount(); 1174 for (int i = 0; i < childCount; i++) { 1175 View child = getChildAt(i); 1176 if (child.getVisibility() != View.GONE) { 1177 return child; 1178 } 1179 } 1180 return null; 1181 } 1182 1183 /** 1184 * @return The first child which has visibility unequal to GONE which is currently below the 1185 * given translationY or equal to it. 1186 */ 1187 private View getFirstChildBelowTranlsationY(float translationY) { 1188 int childCount = getChildCount(); 1189 for (int i = 0; i < childCount; i++) { 1190 View child = getChildAt(i); 1191 if (child.getVisibility() != View.GONE && child.getTranslationY() >= translationY) { 1192 return child; 1193 } 1194 } 1195 return null; 1196 } 1197 1198 /** 1199 * @return the last child which has visibility unequal to GONE 1200 */ 1201 public View getLastChildNotGone() { 1202 int childCount = getChildCount(); 1203 for (int i = childCount - 1; i >= 0; i--) { 1204 View child = getChildAt(i); 1205 if (child.getVisibility() != View.GONE) { 1206 return child; 1207 } 1208 } 1209 return null; 1210 } 1211 1212 /** 1213 * @return the number of children which have visibility unequal to GONE 1214 */ 1215 public int getNotGoneChildCount() { 1216 int childCount = getChildCount(); 1217 int count = 0; 1218 for (int i = 0; i < childCount; i++) { 1219 View child = getChildAt(i); 1220 if (child.getVisibility() != View.GONE) { 1221 count++; 1222 } 1223 } 1224 if (mDismissView.willBeGone()) { 1225 count--; 1226 } 1227 if (mEmptyShadeView.willBeGone()) { 1228 count--; 1229 } 1230 return count; 1231 } 1232 1233 private int getMaxExpandHeight(View view) { 1234 if (view instanceof ExpandableNotificationRow) { 1235 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 1236 return row.getIntrinsicHeight(); 1237 } 1238 return view.getHeight(); 1239 } 1240 1241 public int getContentHeight() { 1242 return mContentHeight; 1243 } 1244 1245 private void updateContentHeight() { 1246 int height = 0; 1247 for (int i = 0; i < getChildCount(); i++) { 1248 View child = getChildAt(i); 1249 if (child.getVisibility() != View.GONE) { 1250 if (height != 0) { 1251 // add the padding before this element 1252 height += mPaddingBetweenElements; 1253 } 1254 if (child instanceof ExpandableNotificationRow) { 1255 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 1256 height += row.getIntrinsicHeight(); 1257 } else if (child instanceof ExpandableView) { 1258 ExpandableView expandableView = (ExpandableView) child; 1259 height += expandableView.getActualHeight(); 1260 } 1261 } 1262 } 1263 mContentHeight = height + mTopPadding; 1264 } 1265 1266 /** 1267 * Fling the scroll view 1268 * 1269 * @param velocityY The initial velocity in the Y direction. Positive 1270 * numbers mean that the finger/cursor is moving down the screen, 1271 * which means we want to scroll towards the top. 1272 */ 1273 private void fling(int velocityY) { 1274 if (getChildCount() > 0) { 1275 int scrollRange = getScrollRange(); 1276 1277 float topAmount = getCurrentOverScrollAmount(true); 1278 float bottomAmount = getCurrentOverScrollAmount(false); 1279 if (velocityY < 0 && topAmount > 0) { 1280 mOwnScrollY -= (int) topAmount; 1281 mDontReportNextOverScroll = true; 1282 setOverScrollAmount(0, true, false); 1283 mMaxOverScroll = Math.abs(velocityY) / 1000f * getRubberBandFactor(true /* onTop */) 1284 * mOverflingDistance + topAmount; 1285 } else if (velocityY > 0 && bottomAmount > 0) { 1286 mOwnScrollY += bottomAmount; 1287 setOverScrollAmount(0, false, false); 1288 mMaxOverScroll = Math.abs(velocityY) / 1000f 1289 * getRubberBandFactor(false /* onTop */) * mOverflingDistance 1290 + bottomAmount; 1291 } else { 1292 // it will be set once we reach the boundary 1293 mMaxOverScroll = 0.0f; 1294 } 1295 mScroller.fling(mScrollX, mOwnScrollY, 1, velocityY, 0, 0, 0, 1296 Math.max(0, scrollRange), 0, Integer.MAX_VALUE / 2); 1297 1298 postInvalidateOnAnimation(); 1299 } 1300 } 1301 1302 /** 1303 * @return Whether a fling performed on the top overscroll edge lead to the expanded 1304 * overScroll view (i.e QS). 1305 */ 1306 private boolean shouldOverScrollFling(int initialVelocity) { 1307 float topOverScroll = getCurrentOverScrollAmount(true); 1308 return mScrolledToTopOnFirstDown 1309 && !mExpandedInThisMotion 1310 && topOverScroll > mMinTopOverScrollToEscape 1311 && initialVelocity > 0; 1312 } 1313 1314 public void updateTopPadding(float qsHeight, int scrollY, boolean animate) { 1315 float start = qsHeight - scrollY + mNotificationTopPadding; 1316 float stackHeight = getHeight() - start; 1317 int minStackHeight = getMinStackHeight(); 1318 if (stackHeight <= minStackHeight) { 1319 float overflow = minStackHeight - stackHeight; 1320 stackHeight = minStackHeight; 1321 start = getHeight() - stackHeight; 1322 setTranslationY(overflow); 1323 mTopPaddingOverflow = overflow; 1324 } else { 1325 setTranslationY(0); 1326 mTopPaddingOverflow = 0; 1327 } 1328 setTopPadding(clampPadding((int) start), animate); 1329 } 1330 1331 public int getNotificationTopPadding() { 1332 return mNotificationTopPadding; 1333 } 1334 1335 public int getMinStackHeight() { 1336 return mCollapsedSize + mBottomStackPeekSize + mCollapseSecondCardPadding; 1337 } 1338 1339 public float getTopPaddingOverflow() { 1340 return mTopPaddingOverflow; 1341 } 1342 1343 public int getPeekHeight() { 1344 return mIntrinsicPadding + mCollapsedSize + mBottomStackPeekSize 1345 + mCollapseSecondCardPadding; 1346 } 1347 1348 private int clampPadding(int desiredPadding) { 1349 return Math.max(desiredPadding, mIntrinsicPadding); 1350 } 1351 1352 private float getRubberBandFactor(boolean onTop) { 1353 if (!onTop) { 1354 return RUBBER_BAND_FACTOR_NORMAL; 1355 } 1356 if (mExpandedInThisMotion) { 1357 return RUBBER_BAND_FACTOR_AFTER_EXPAND; 1358 } else if (mIsExpansionChanging) { 1359 return RUBBER_BAND_FACTOR_ON_PANEL_EXPAND; 1360 } else if (mScrolledToTopOnFirstDown) { 1361 return 1.0f; 1362 } 1363 return RUBBER_BAND_FACTOR_NORMAL; 1364 } 1365 1366 /** 1367 * Accompanying function for {@link #getRubberBandFactor}: Returns true if the overscroll is 1368 * rubberbanded, false if it is technically an overscroll but rather a motion to expand the 1369 * overscroll view (e.g. expand QS). 1370 */ 1371 private boolean isRubberbanded(boolean onTop) { 1372 return !onTop || mExpandedInThisMotion || mIsExpansionChanging 1373 || !mScrolledToTopOnFirstDown; 1374 } 1375 1376 private void endDrag() { 1377 setIsBeingDragged(false); 1378 1379 recycleVelocityTracker(); 1380 1381 if (getCurrentOverScrollAmount(true /* onTop */) > 0) { 1382 setOverScrollAmount(0, true /* onTop */, true /* animate */); 1383 } 1384 if (getCurrentOverScrollAmount(false /* onTop */) > 0) { 1385 setOverScrollAmount(0, false /* onTop */, true /* animate */); 1386 } 1387 } 1388 1389 private void transformTouchEvent(MotionEvent ev, View sourceView, View targetView) { 1390 ev.offsetLocation(sourceView.getX(), sourceView.getY()); 1391 ev.offsetLocation(-targetView.getX(), -targetView.getY()); 1392 } 1393 1394 @Override 1395 public boolean onInterceptTouchEvent(MotionEvent ev) { 1396 if (mInterceptDelegateEnabled) { 1397 transformTouchEvent(ev, this, mScrollView); 1398 if (mScrollView.onInterceptTouchEvent(ev)) { 1399 mDelegateToScrollView = true; 1400 removeLongPressCallback(); 1401 return true; 1402 } 1403 transformTouchEvent(ev, mScrollView, this); 1404 } 1405 initDownStates(ev); 1406 boolean expandWantsIt = false; 1407 if (!mSwipingInProgress && !mOnlyScrollingInThisMotion && isScrollingEnabled()) { 1408 expandWantsIt = mExpandHelper.onInterceptTouchEvent(ev); 1409 } 1410 boolean scrollWantsIt = false; 1411 if (!mSwipingInProgress && !mExpandingNotification) { 1412 scrollWantsIt = onInterceptTouchEventScroll(ev); 1413 } 1414 boolean swipeWantsIt = false; 1415 if (!mIsBeingDragged 1416 && !mExpandingNotification 1417 && !mExpandedInThisMotion 1418 && !mOnlyScrollingInThisMotion) { 1419 swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev); 1420 } 1421 return swipeWantsIt || scrollWantsIt || expandWantsIt || super.onInterceptTouchEvent(ev); 1422 } 1423 1424 private void initDownStates(MotionEvent ev) { 1425 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 1426 mExpandedInThisMotion = false; 1427 mOnlyScrollingInThisMotion = !mScroller.isFinished(); 1428 mDisallowScrollingInThisMotion = false; 1429 } 1430 } 1431 1432 @Override 1433 protected void onViewRemoved(View child) { 1434 super.onViewRemoved(child); 1435 mStackScrollAlgorithm.notifyChildrenChanged(this); 1436 if (mChangePositionInProgress) { 1437 // This is only a position change, don't do anything special 1438 return; 1439 } 1440 ((ExpandableView) child).setOnHeightChangedListener(null); 1441 mCurrentStackScrollState.removeViewStateForView(child); 1442 updateScrollStateForRemovedChild(child); 1443 boolean animationGenerated = generateRemoveAnimation(child); 1444 if (animationGenerated && !mSwipedOutViews.contains(child)) { 1445 // Add this view to an overlay in order to ensure that it will still be temporary 1446 // drawn when removed 1447 getOverlay().add(child); 1448 } 1449 } 1450 1451 /** 1452 * Generate a remove animation for a child view. 1453 * 1454 * @param child The view to generate the remove animation for. 1455 * @return Whether an animation was generated. 1456 */ 1457 private boolean generateRemoveAnimation(View child) { 1458 if (mIsExpanded && mAnimationsEnabled) { 1459 if (!mChildrenToAddAnimated.contains(child)) { 1460 // Generate Animations 1461 mChildrenToRemoveAnimated.add(child); 1462 mNeedsAnimation = true; 1463 return true; 1464 } else { 1465 mChildrenToAddAnimated.remove(child); 1466 mFromMoreCardAdditions.remove(child); 1467 return false; 1468 } 1469 } 1470 return false; 1471 } 1472 1473 /** 1474 * Updates the scroll position when a child was removed 1475 * 1476 * @param removedChild the removed child 1477 */ 1478 private void updateScrollStateForRemovedChild(View removedChild) { 1479 int startingPosition = getPositionInLinearLayout(removedChild); 1480 int childHeight = getIntrinsicHeight(removedChild) + mPaddingBetweenElements; 1481 int endPosition = startingPosition + childHeight; 1482 if (endPosition <= mOwnScrollY) { 1483 // This child is fully scrolled of the top, so we have to deduct its height from the 1484 // scrollPosition 1485 mOwnScrollY -= childHeight; 1486 } else if (startingPosition < mOwnScrollY) { 1487 // This child is currently being scrolled into, set the scroll position to the start of 1488 // this child 1489 mOwnScrollY = startingPosition; 1490 } 1491 } 1492 1493 private int getIntrinsicHeight(View view) { 1494 if (view instanceof ExpandableView) { 1495 ExpandableView expandableView = (ExpandableView) view; 1496 return expandableView.getIntrinsicHeight(); 1497 } 1498 return view.getHeight(); 1499 } 1500 1501 private int getPositionInLinearLayout(View requestedChild) { 1502 int position = 0; 1503 for (int i = 0; i < getChildCount(); i++) { 1504 View child = getChildAt(i); 1505 if (child == requestedChild) { 1506 return position; 1507 } 1508 if (child.getVisibility() != View.GONE) { 1509 position += child.getHeight(); 1510 if (i < getChildCount()-1) { 1511 position += mPaddingBetweenElements; 1512 } 1513 } 1514 } 1515 return 0; 1516 } 1517 1518 @Override 1519 protected void onViewAdded(View child) { 1520 super.onViewAdded(child); 1521 mStackScrollAlgorithm.notifyChildrenChanged(this); 1522 ((ExpandableView) child).setOnHeightChangedListener(this); 1523 generateAddAnimation(child, false /* fromMoreCard */); 1524 } 1525 1526 public void setAnimationsEnabled(boolean animationsEnabled) { 1527 mAnimationsEnabled = animationsEnabled; 1528 } 1529 1530 public boolean isAddOrRemoveAnimationPending() { 1531 return mNeedsAnimation 1532 && (!mChildrenToAddAnimated.isEmpty() || !mChildrenToRemoveAnimated.isEmpty()); 1533 } 1534 /** 1535 * Generate an animation for an added child view. 1536 * 1537 * @param child The view to be added. 1538 * @param fromMoreCard Whether this add is coming from the "more" card on lockscreen. 1539 */ 1540 public void generateAddAnimation(View child, boolean fromMoreCard) { 1541 if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress) { 1542 // Generate Animations 1543 mChildrenToAddAnimated.add(child); 1544 if (fromMoreCard) { 1545 mFromMoreCardAdditions.add(child); 1546 } 1547 mNeedsAnimation = true; 1548 } 1549 } 1550 1551 /** 1552 * Change the position of child to a new location 1553 * 1554 * @param child the view to change the position for 1555 * @param newIndex the new index 1556 */ 1557 public void changeViewPosition(View child, int newIndex) { 1558 int currentIndex = indexOfChild(child); 1559 if (child != null && child.getParent() == this && currentIndex != newIndex) { 1560 mChangePositionInProgress = true; 1561 removeView(child); 1562 addView(child, newIndex); 1563 mChangePositionInProgress = false; 1564 if (mIsExpanded && mAnimationsEnabled && child.getVisibility() != View.GONE) { 1565 mChildrenChangingPositions.add(child); 1566 mNeedsAnimation = true; 1567 } 1568 } 1569 } 1570 1571 private void startAnimationToState() { 1572 if (mNeedsAnimation) { 1573 generateChildHierarchyEvents(); 1574 mNeedsAnimation = false; 1575 } 1576 if (!mAnimationEvents.isEmpty() || isCurrentlyAnimating()) { 1577 mStateAnimator.startAnimationForEvents(mAnimationEvents, mCurrentStackScrollState, 1578 mGoToFullShadeDelay); 1579 mAnimationEvents.clear(); 1580 } else { 1581 applyCurrentState(); 1582 } 1583 mGoToFullShadeDelay = 0; 1584 } 1585 1586 private void generateChildHierarchyEvents() { 1587 generateChildRemovalEvents(); 1588 generateChildAdditionEvents(); 1589 generatePositionChangeEvents(); 1590 generateSnapBackEvents(); 1591 generateDragEvents(); 1592 generateTopPaddingEvent(); 1593 generateActivateEvent(); 1594 generateDimmedEvent(); 1595 generateHideSensitiveEvent(); 1596 generateDarkEvent(); 1597 generateGoToFullShadeEvent(); 1598 mNeedsAnimation = false; 1599 } 1600 1601 private void generateSnapBackEvents() { 1602 for (View child : mSnappedBackChildren) { 1603 mAnimationEvents.add(new AnimationEvent(child, 1604 AnimationEvent.ANIMATION_TYPE_SNAP_BACK)); 1605 } 1606 mSnappedBackChildren.clear(); 1607 } 1608 1609 private void generateDragEvents() { 1610 for (View child : mDragAnimPendingChildren) { 1611 mAnimationEvents.add(new AnimationEvent(child, 1612 AnimationEvent.ANIMATION_TYPE_START_DRAG)); 1613 } 1614 mDragAnimPendingChildren.clear(); 1615 } 1616 1617 private void generateChildRemovalEvents() { 1618 for (View child : mChildrenToRemoveAnimated) { 1619 boolean childWasSwipedOut = mSwipedOutViews.contains(child); 1620 int animationType = childWasSwipedOut 1621 ? AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT 1622 : AnimationEvent.ANIMATION_TYPE_REMOVE; 1623 AnimationEvent event = new AnimationEvent(child, animationType); 1624 1625 // we need to know the view after this one 1626 event.viewAfterChangingView = getFirstChildBelowTranlsationY(child.getTranslationY()); 1627 mAnimationEvents.add(event); 1628 } 1629 mSwipedOutViews.clear(); 1630 mChildrenToRemoveAnimated.clear(); 1631 } 1632 1633 private void generatePositionChangeEvents() { 1634 for (View child : mChildrenChangingPositions) { 1635 mAnimationEvents.add(new AnimationEvent(child, 1636 AnimationEvent.ANIMATION_TYPE_CHANGE_POSITION)); 1637 } 1638 mChildrenChangingPositions.clear(); 1639 } 1640 1641 private void generateChildAdditionEvents() { 1642 for (View child : mChildrenToAddAnimated) { 1643 if (mFromMoreCardAdditions.contains(child)) { 1644 mAnimationEvents.add(new AnimationEvent(child, 1645 AnimationEvent.ANIMATION_TYPE_ADD, 1646 StackStateAnimator.ANIMATION_DURATION_STANDARD)); 1647 } else { 1648 mAnimationEvents.add(new AnimationEvent(child, 1649 AnimationEvent.ANIMATION_TYPE_ADD)); 1650 } 1651 } 1652 mChildrenToAddAnimated.clear(); 1653 mFromMoreCardAdditions.clear(); 1654 } 1655 1656 private void generateTopPaddingEvent() { 1657 if (mTopPaddingNeedsAnimation) { 1658 mAnimationEvents.add( 1659 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED)); 1660 } 1661 mTopPaddingNeedsAnimation = false; 1662 } 1663 1664 private void generateActivateEvent() { 1665 if (mActivateNeedsAnimation) { 1666 mAnimationEvents.add( 1667 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_ACTIVATED_CHILD)); 1668 } 1669 mActivateNeedsAnimation = false; 1670 } 1671 1672 private void generateDimmedEvent() { 1673 if (mDimmedNeedsAnimation) { 1674 mAnimationEvents.add( 1675 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DIMMED)); 1676 } 1677 mDimmedNeedsAnimation = false; 1678 } 1679 1680 private void generateHideSensitiveEvent() { 1681 if (mHideSensitiveNeedsAnimation) { 1682 mAnimationEvents.add( 1683 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_HIDE_SENSITIVE)); 1684 } 1685 mHideSensitiveNeedsAnimation = false; 1686 } 1687 1688 private void generateDarkEvent() { 1689 if (mDarkNeedsAnimation) { 1690 mAnimationEvents.add( 1691 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_DARK)); 1692 } 1693 mDarkNeedsAnimation = false; 1694 } 1695 1696 private void generateGoToFullShadeEvent() { 1697 if (mGoToFullShadeNeedsAnimation) { 1698 mAnimationEvents.add( 1699 new AnimationEvent(null, AnimationEvent.ANIMATION_TYPE_GO_TO_FULL_SHADE)); 1700 } 1701 mGoToFullShadeNeedsAnimation = false; 1702 } 1703 1704 private boolean onInterceptTouchEventScroll(MotionEvent ev) { 1705 if (!isScrollingEnabled()) { 1706 return false; 1707 } 1708 /* 1709 * This method JUST determines whether we want to intercept the motion. 1710 * If we return true, onMotionEvent will be called and we do the actual 1711 * scrolling there. 1712 */ 1713 1714 /* 1715 * Shortcut the most recurring case: the user is in the dragging 1716 * state and he is moving his finger. We want to intercept this 1717 * motion. 1718 */ 1719 final int action = ev.getAction(); 1720 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 1721 return true; 1722 } 1723 1724 switch (action & MotionEvent.ACTION_MASK) { 1725 case MotionEvent.ACTION_MOVE: { 1726 /* 1727 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 1728 * whether the user has moved far enough from his original down touch. 1729 */ 1730 1731 /* 1732 * Locally do absolute value. mLastMotionY is set to the y value 1733 * of the down event. 1734 */ 1735 final int activePointerId = mActivePointerId; 1736 if (activePointerId == INVALID_POINTER) { 1737 // If we don't have a valid id, the touch down wasn't on content. 1738 break; 1739 } 1740 1741 final int pointerIndex = ev.findPointerIndex(activePointerId); 1742 if (pointerIndex == -1) { 1743 Log.e(TAG, "Invalid pointerId=" + activePointerId 1744 + " in onInterceptTouchEvent"); 1745 break; 1746 } 1747 1748 final int y = (int) ev.getY(pointerIndex); 1749 final int x = (int) ev.getX(pointerIndex); 1750 final int yDiff = Math.abs(y - mLastMotionY); 1751 final int xDiff = Math.abs(x - mDownX); 1752 if (yDiff > mTouchSlop && yDiff > xDiff) { 1753 setIsBeingDragged(true); 1754 mLastMotionY = y; 1755 mDownX = x; 1756 initVelocityTrackerIfNotExists(); 1757 mVelocityTracker.addMovement(ev); 1758 } 1759 break; 1760 } 1761 1762 case MotionEvent.ACTION_DOWN: { 1763 final int y = (int) ev.getY(); 1764 if (getChildAtPosition(ev.getX(), y) == null) { 1765 setIsBeingDragged(false); 1766 recycleVelocityTracker(); 1767 break; 1768 } 1769 1770 /* 1771 * Remember location of down touch. 1772 * ACTION_DOWN always refers to pointer index 0. 1773 */ 1774 mLastMotionY = y; 1775 mDownX = (int) ev.getX(); 1776 mActivePointerId = ev.getPointerId(0); 1777 mScrolledToTopOnFirstDown = isScrolledToTop(); 1778 1779 initOrResetVelocityTracker(); 1780 mVelocityTracker.addMovement(ev); 1781 /* 1782 * If being flinged and user touches the screen, initiate drag; 1783 * otherwise don't. mScroller.isFinished should be false when 1784 * being flinged. 1785 */ 1786 boolean isBeingDragged = !mScroller.isFinished(); 1787 setIsBeingDragged(isBeingDragged); 1788 break; 1789 } 1790 1791 case MotionEvent.ACTION_CANCEL: 1792 case MotionEvent.ACTION_UP: 1793 /* Release the drag */ 1794 setIsBeingDragged(false); 1795 mActivePointerId = INVALID_POINTER; 1796 recycleVelocityTracker(); 1797 if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) { 1798 postInvalidateOnAnimation(); 1799 } 1800 break; 1801 case MotionEvent.ACTION_POINTER_UP: 1802 onSecondaryPointerUp(ev); 1803 break; 1804 } 1805 1806 /* 1807 * The only time we want to intercept motion events is if we are in the 1808 * drag mode. 1809 */ 1810 return mIsBeingDragged; 1811 } 1812 1813 /** 1814 * @return Whether the specified motion event is actually happening over the content. 1815 */ 1816 private boolean isInContentBounds(MotionEvent event) { 1817 return event.getY() < getHeight() - getEmptyBottomMargin(); 1818 } 1819 1820 private void setIsBeingDragged(boolean isDragged) { 1821 mIsBeingDragged = isDragged; 1822 if (isDragged) { 1823 requestDisallowInterceptTouchEvent(true); 1824 removeLongPressCallback(); 1825 } 1826 } 1827 1828 @Override 1829 public void onWindowFocusChanged(boolean hasWindowFocus) { 1830 super.onWindowFocusChanged(hasWindowFocus); 1831 if (!hasWindowFocus) { 1832 removeLongPressCallback(); 1833 } 1834 } 1835 1836 public void removeLongPressCallback() { 1837 mSwipeHelper.removeLongPressCallback(); 1838 } 1839 1840 @Override 1841 public boolean isScrolledToTop() { 1842 return mOwnScrollY == 0; 1843 } 1844 1845 @Override 1846 public boolean isScrolledToBottom() { 1847 return mOwnScrollY >= getScrollRange(); 1848 } 1849 1850 @Override 1851 public View getHostView() { 1852 return this; 1853 } 1854 1855 public int getEmptyBottomMargin() { 1856 int emptyMargin = mMaxLayoutHeight - mContentHeight - mBottomStackPeekSize; 1857 if (needsHeightAdaption()) { 1858 emptyMargin -= mBottomStackSlowDownHeight; 1859 } else { 1860 emptyMargin -= mCollapseSecondCardPadding; 1861 } 1862 return Math.max(emptyMargin, 0); 1863 } 1864 1865 public void onExpansionStarted() { 1866 mIsExpansionChanging = true; 1867 mStackScrollAlgorithm.onExpansionStarted(mCurrentStackScrollState); 1868 } 1869 1870 public void onExpansionStopped() { 1871 mIsExpansionChanging = false; 1872 mStackScrollAlgorithm.onExpansionStopped(); 1873 if (!mIsExpanded) { 1874 mOwnScrollY = 0; 1875 } 1876 } 1877 1878 private void setIsExpanded(boolean isExpanded) { 1879 mIsExpanded = isExpanded; 1880 mStackScrollAlgorithm.setIsExpanded(isExpanded); 1881 } 1882 1883 @Override 1884 public void onHeightChanged(ExpandableView view) { 1885 updateContentHeight(); 1886 updateScrollPositionOnExpandInBottom(view); 1887 clampScrollPosition(); 1888 notifyHeightChangeListener(view); 1889 requestChildrenUpdate(); 1890 } 1891 1892 private void updateScrollPositionOnExpandInBottom(ExpandableView view) { 1893 if (view instanceof ExpandableNotificationRow) { 1894 ExpandableNotificationRow row = (ExpandableNotificationRow) view; 1895 if (row.isUserLocked()) { 1896 // We are actually expanding this view 1897 float endPosition = row.getTranslationY() + row.getActualHeight(); 1898 int stackEnd = mMaxLayoutHeight - mBottomStackPeekSize - 1899 mBottomStackSlowDownHeight; 1900 if (endPosition > stackEnd) { 1901 mOwnScrollY += endPosition - stackEnd; 1902 mDisallowScrollingInThisMotion = true; 1903 } 1904 } 1905 } 1906 } 1907 1908 public void setOnHeightChangedListener( 1909 ExpandableView.OnHeightChangedListener mOnHeightChangedListener) { 1910 this.mOnHeightChangedListener = mOnHeightChangedListener; 1911 } 1912 1913 public void onChildAnimationFinished() { 1914 requestChildrenUpdate(); 1915 } 1916 1917 /** 1918 * See {@link AmbientState#setDimmed}. 1919 */ 1920 public void setDimmed(boolean dimmed, boolean animate) { 1921 mStackScrollAlgorithm.setDimmed(dimmed); 1922 mAmbientState.setDimmed(dimmed); 1923 updatePadding(dimmed); 1924 if (animate && mAnimationsEnabled) { 1925 mDimmedNeedsAnimation = true; 1926 mNeedsAnimation = true; 1927 } 1928 requestChildrenUpdate(); 1929 } 1930 1931 public void setHideSensitive(boolean hideSensitive, boolean animate) { 1932 if (hideSensitive != mAmbientState.isHideSensitive()) { 1933 int childCount = getChildCount(); 1934 for (int i = 0; i < childCount; i++) { 1935 ExpandableView v = (ExpandableView) getChildAt(i); 1936 v.setHideSensitiveForIntrinsicHeight(hideSensitive); 1937 } 1938 mAmbientState.setHideSensitive(hideSensitive); 1939 if (animate && mAnimationsEnabled) { 1940 mHideSensitiveNeedsAnimation = true; 1941 mNeedsAnimation = true; 1942 } 1943 requestChildrenUpdate(); 1944 } 1945 } 1946 1947 /** 1948 * See {@link AmbientState#setActivatedChild}. 1949 */ 1950 public void setActivatedChild(ActivatableNotificationView activatedChild) { 1951 mAmbientState.setActivatedChild(activatedChild); 1952 if (mAnimationsEnabled) { 1953 mActivateNeedsAnimation = true; 1954 mNeedsAnimation = true; 1955 } 1956 requestChildrenUpdate(); 1957 } 1958 1959 public ActivatableNotificationView getActivatedChild() { 1960 return mAmbientState.getActivatedChild(); 1961 } 1962 1963 private void applyCurrentState() { 1964 mCurrentStackScrollState.apply(); 1965 if (mListener != null) { 1966 mListener.onChildLocationsChanged(this); 1967 } 1968 } 1969 1970 public void setSpeedBumpView(SpeedBumpView speedBumpView) { 1971 mSpeedBumpView = speedBumpView; 1972 addView(speedBumpView); 1973 } 1974 1975 private void updateSpeedBump(boolean visible) { 1976 boolean notGoneBefore = mSpeedBumpView.getVisibility() != GONE; 1977 if (visible != notGoneBefore) { 1978 int newVisibility = visible ? VISIBLE : GONE; 1979 mSpeedBumpView.setVisibility(newVisibility); 1980 if (visible) { 1981 // Make invisible to ensure that the appear animation is played. 1982 mSpeedBumpView.setInvisible(); 1983 } else { 1984 // TODO: This doesn't really work, because the view is already set to GONE above. 1985 generateRemoveAnimation(mSpeedBumpView); 1986 } 1987 } 1988 } 1989 1990 public void goToFullShade(long delay) { 1991 updateSpeedBump(true /* visibility */); 1992 mDismissView.setInvisible(); 1993 mEmptyShadeView.setInvisible(); 1994 mGoToFullShadeNeedsAnimation = true; 1995 mGoToFullShadeDelay = delay; 1996 mNeedsAnimation = true; 1997 requestChildrenUpdate(); 1998 } 1999 2000 public void cancelExpandHelper() { 2001 mExpandHelper.cancel(); 2002 } 2003 2004 public void setIntrinsicPadding(int intrinsicPadding) { 2005 mIntrinsicPadding = intrinsicPadding; 2006 } 2007 2008 public int getIntrinsicPadding() { 2009 return mIntrinsicPadding; 2010 } 2011 2012 /** 2013 * @return the y position of the first notification 2014 */ 2015 public float getNotificationsTopY() { 2016 return mTopPadding + getTranslationY(); 2017 } 2018 2019 @Override 2020 public boolean shouldDelayChildPressedState() { 2021 return true; 2022 } 2023 2024 public void setScrimAlpha(float progress) { 2025 if (progress != mAmbientState.getScrimAmount()) { 2026 mAmbientState.setScrimAmount(progress); 2027 requestChildrenUpdate(); 2028 } 2029 } 2030 2031 /** 2032 * See {@link AmbientState#setDark}. 2033 */ 2034 public void setDark(boolean dark, boolean animate) { 2035 mAmbientState.setDark(dark); 2036 if (animate && mAnimationsEnabled) { 2037 mDarkNeedsAnimation = true; 2038 mNeedsAnimation = true; 2039 } 2040 requestChildrenUpdate(); 2041 } 2042 2043 public void setDismissView(DismissView dismissView) { 2044 mDismissView = dismissView; 2045 addView(mDismissView); 2046 } 2047 2048 public void setEmptyShadeView(EmptyShadeView emptyShadeView) { 2049 mEmptyShadeView = emptyShadeView; 2050 addView(mEmptyShadeView); 2051 } 2052 2053 public void updateEmptyShadeView(boolean visible) { 2054 int oldVisibility = mEmptyShadeView.willBeGone() ? GONE : mEmptyShadeView.getVisibility(); 2055 int newVisibility = visible ? VISIBLE : GONE; 2056 if (oldVisibility != newVisibility) { 2057 if (oldVisibility == GONE) { 2058 if (mEmptyShadeView.willBeGone()) { 2059 mEmptyShadeView.cancelAnimation(); 2060 } else { 2061 mEmptyShadeView.setInvisible(); 2062 mEmptyShadeView.setVisibility(newVisibility); 2063 } 2064 mEmptyShadeView.setWillBeGone(false); 2065 updateContentHeight(); 2066 } else { 2067 mEmptyShadeView.setWillBeGone(true); 2068 mEmptyShadeView.performVisibilityAnimation(false, new Runnable() { 2069 @Override 2070 public void run() { 2071 mEmptyShadeView.setVisibility(GONE); 2072 mEmptyShadeView.setWillBeGone(false); 2073 updateContentHeight(); 2074 } 2075 }); 2076 } 2077 } 2078 } 2079 2080 public void updateDismissView(boolean visible) { 2081 int oldVisibility = mDismissView.willBeGone() ? GONE : mDismissView.getVisibility(); 2082 int newVisibility = visible ? VISIBLE : GONE; 2083 if (oldVisibility != newVisibility) { 2084 if (oldVisibility == GONE) { 2085 if (mDismissView.willBeGone()) { 2086 mDismissView.cancelAnimation(); 2087 } else { 2088 mDismissView.setInvisible(); 2089 mDismissView.setVisibility(newVisibility); 2090 } 2091 mDismissView.setWillBeGone(false); 2092 updateContentHeight(); 2093 } else { 2094 mDismissView.setWillBeGone(true); 2095 mDismissView.performVisibilityAnimation(false, new Runnable() { 2096 @Override 2097 public void run() { 2098 mDismissView.setVisibility(GONE); 2099 mDismissView.setWillBeGone(false); 2100 updateContentHeight(); 2101 } 2102 }); 2103 } 2104 } 2105 } 2106 2107 public void setDismissAllInProgress(boolean dismissAllInProgress) { 2108 mDismissAllInProgress = dismissAllInProgress; 2109 } 2110 2111 public boolean isDismissViewNotGone() { 2112 return mDismissView.getVisibility() != View.GONE && !mDismissView.willBeGone(); 2113 } 2114 2115 public boolean isDismissViewVisible() { 2116 return mDismissView.isVisible(); 2117 } 2118 2119 public int getDismissViewHeight() { 2120 return mDismissView.getHeight() + mPaddingBetweenElementsNormal; 2121 } 2122 2123 /** 2124 * A listener that is notified when some child locations might have changed. 2125 */ 2126 public interface OnChildLocationsChangedListener { 2127 public void onChildLocationsChanged(NotificationStackScrollLayout stackScrollLayout); 2128 } 2129 2130 /** 2131 * A listener that gets notified when the overscroll at the top has changed. 2132 */ 2133 public interface OnOverscrollTopChangedListener { 2134 2135 /** 2136 * Notifies a listener that the overscroll has changed. 2137 * 2138 * @param amount the amount of overscroll, in pixels 2139 * @param isRubberbanded if true, this is a rubberbanded overscroll; if false, this is an 2140 * unrubberbanded motion to directly expand overscroll view (e.g expand 2141 * QS) 2142 */ 2143 public void onOverscrollTopChanged(float amount, boolean isRubberbanded); 2144 2145 /** 2146 * Notify a listener that the scroller wants to escape from the scrolling motion and 2147 * start a fling animation to the expanded or collapsed overscroll view (e.g expand the QS) 2148 * 2149 * @param velocity The velocity that the Scroller had when over flinging 2150 * @param open Should the fling open or close the overscroll view. 2151 */ 2152 public void flingTopOverscroll(float velocity, boolean open); 2153 } 2154 2155 static class AnimationEvent { 2156 2157 static AnimationFilter[] FILTERS = new AnimationFilter[] { 2158 2159 // ANIMATION_TYPE_ADD 2160 new AnimationFilter() 2161 .animateAlpha() 2162 .animateHeight() 2163 .animateTopInset() 2164 .animateY() 2165 .animateZ() 2166 .hasDelays(), 2167 2168 // ANIMATION_TYPE_REMOVE 2169 new AnimationFilter() 2170 .animateAlpha() 2171 .animateHeight() 2172 .animateTopInset() 2173 .animateY() 2174 .animateZ() 2175 .hasDelays(), 2176 2177 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 2178 new AnimationFilter() 2179 .animateAlpha() 2180 .animateHeight() 2181 .animateTopInset() 2182 .animateY() 2183 .animateZ() 2184 .hasDelays(), 2185 2186 // ANIMATION_TYPE_TOP_PADDING_CHANGED 2187 new AnimationFilter() 2188 .animateAlpha() 2189 .animateHeight() 2190 .animateTopInset() 2191 .animateY() 2192 .animateDimmed() 2193 .animateScale() 2194 .animateZ(), 2195 2196 // ANIMATION_TYPE_START_DRAG 2197 new AnimationFilter() 2198 .animateAlpha(), 2199 2200 // ANIMATION_TYPE_SNAP_BACK 2201 new AnimationFilter() 2202 .animateAlpha() 2203 .animateHeight(), 2204 2205 // ANIMATION_TYPE_ACTIVATED_CHILD 2206 new AnimationFilter() 2207 .animateScale() 2208 .animateAlpha(), 2209 2210 // ANIMATION_TYPE_DIMMED 2211 new AnimationFilter() 2212 .animateY() 2213 .animateScale() 2214 .animateDimmed(), 2215 2216 // ANIMATION_TYPE_CHANGE_POSITION 2217 new AnimationFilter() 2218 .animateAlpha() 2219 .animateHeight() 2220 .animateTopInset() 2221 .animateY() 2222 .animateZ(), 2223 2224 // ANIMATION_TYPE_DARK 2225 new AnimationFilter() 2226 .animateDark(), 2227 2228 // ANIMATION_TYPE_GO_TO_FULL_SHADE 2229 new AnimationFilter() 2230 .animateAlpha() 2231 .animateHeight() 2232 .animateTopInset() 2233 .animateY() 2234 .animateDimmed() 2235 .animateScale() 2236 .animateZ() 2237 .hasDelays(), 2238 2239 // ANIMATION_TYPE_HIDE_SENSITIVE 2240 new AnimationFilter() 2241 .animateHideSensitive(), 2242 }; 2243 2244 static int[] LENGTHS = new int[] { 2245 2246 // ANIMATION_TYPE_ADD 2247 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 2248 2249 // ANIMATION_TYPE_REMOVE 2250 StackStateAnimator.ANIMATION_DURATION_APPEAR_DISAPPEAR, 2251 2252 // ANIMATION_TYPE_REMOVE_SWIPED_OUT 2253 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2254 2255 // ANIMATION_TYPE_TOP_PADDING_CHANGED 2256 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2257 2258 // ANIMATION_TYPE_START_DRAG 2259 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2260 2261 // ANIMATION_TYPE_SNAP_BACK 2262 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2263 2264 // ANIMATION_TYPE_ACTIVATED_CHILD 2265 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 2266 2267 // ANIMATION_TYPE_DIMMED 2268 StackStateAnimator.ANIMATION_DURATION_DIMMED_ACTIVATED, 2269 2270 // ANIMATION_TYPE_CHANGE_POSITION 2271 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2272 2273 // ANIMATION_TYPE_DARK 2274 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2275 2276 // ANIMATION_TYPE_GO_TO_FULL_SHADE 2277 StackStateAnimator.ANIMATION_DURATION_GO_TO_FULL_SHADE, 2278 2279 // ANIMATION_TYPE_HIDE_SENSITIVE 2280 StackStateAnimator.ANIMATION_DURATION_STANDARD, 2281 }; 2282 2283 static final int ANIMATION_TYPE_ADD = 0; 2284 static final int ANIMATION_TYPE_REMOVE = 1; 2285 static final int ANIMATION_TYPE_REMOVE_SWIPED_OUT = 2; 2286 static final int ANIMATION_TYPE_TOP_PADDING_CHANGED = 3; 2287 static final int ANIMATION_TYPE_START_DRAG = 4; 2288 static final int ANIMATION_TYPE_SNAP_BACK = 5; 2289 static final int ANIMATION_TYPE_ACTIVATED_CHILD = 6; 2290 static final int ANIMATION_TYPE_DIMMED = 7; 2291 static final int ANIMATION_TYPE_CHANGE_POSITION = 8; 2292 static final int ANIMATION_TYPE_DARK = 9; 2293 static final int ANIMATION_TYPE_GO_TO_FULL_SHADE = 10; 2294 static final int ANIMATION_TYPE_HIDE_SENSITIVE = 11; 2295 2296 final long eventStartTime; 2297 final View changingView; 2298 final int animationType; 2299 final AnimationFilter filter; 2300 final long length; 2301 View viewAfterChangingView; 2302 2303 AnimationEvent(View view, int type) { 2304 this(view, type, LENGTHS[type]); 2305 } 2306 2307 AnimationEvent(View view, int type, long length) { 2308 eventStartTime = AnimationUtils.currentAnimationTimeMillis(); 2309 changingView = view; 2310 animationType = type; 2311 filter = FILTERS[type]; 2312 this.length = length; 2313 } 2314 2315 /** 2316 * Combines the length of several animation events into a single value. 2317 * 2318 * @param events The events of the lengths to combine. 2319 * @return The combined length. Depending on the event types, this might be the maximum of 2320 * all events or the length of a specific event. 2321 */ 2322 static long combineLength(ArrayList<AnimationEvent> events) { 2323 long length = 0; 2324 int size = events.size(); 2325 for (int i = 0; i < size; i++) { 2326 AnimationEvent event = events.get(i); 2327 length = Math.max(length, event.length); 2328 if (event.animationType == ANIMATION_TYPE_GO_TO_FULL_SHADE) { 2329 return event.length; 2330 } 2331 } 2332 return length; 2333 } 2334 } 2335 2336} 2337