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