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