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