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