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.notification.stack; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.util.Log; 24 import android.util.MathUtils; 25 import android.view.View; 26 import android.view.ViewGroup; 27 28 import com.android.systemui.R; 29 import com.android.systemui.statusbar.EmptyShadeView; 30 import com.android.systemui.statusbar.NotificationShelf; 31 import com.android.systemui.statusbar.notification.NotificationUtils; 32 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 33 import com.android.systemui.statusbar.notification.row.ExpandableView; 34 import com.android.systemui.statusbar.notification.row.FooterView; 35 36 import java.util.ArrayList; 37 import java.util.HashMap; 38 import java.util.List; 39 40 /** 41 * The Algorithm of the {@link com.android.systemui.statusbar.notification.stack 42 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar 43 * .stack.StackScrollState} 44 */ 45 public class StackScrollAlgorithm { 46 47 static final boolean ANCHOR_SCROLLING = false; 48 49 private static final String LOG_TAG = "StackScrollAlgorithm"; 50 private final ViewGroup mHostView; 51 52 private int mPaddingBetweenElements; 53 private int mIncreasedPaddingBetweenElements; 54 private int mGapHeight; 55 private int mCollapsedSize; 56 57 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 58 private boolean mIsExpanded; 59 private boolean mClipNotificationScrollToTop; 60 private int mStatusBarHeight; 61 private float mHeadsUpInset; 62 private int mPinnedZTranslationExtra; 63 StackScrollAlgorithm( Context context, ViewGroup hostView)64 public StackScrollAlgorithm( 65 Context context, 66 ViewGroup hostView) { 67 mHostView = hostView; 68 initView(context); 69 } 70 initView(Context context)71 public void initView(Context context) { 72 initConstants(context); 73 } 74 initConstants(Context context)75 private void initConstants(Context context) { 76 Resources res = context.getResources(); 77 mPaddingBetweenElements = res.getDimensionPixelSize( 78 R.dimen.notification_divider_height); 79 mIncreasedPaddingBetweenElements = 80 res.getDimensionPixelSize(R.dimen.notification_divider_height_increased); 81 mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height); 82 mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height); 83 mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop); 84 mHeadsUpInset = mStatusBarHeight + res.getDimensionPixelSize( 85 R.dimen.heads_up_status_bar_padding); 86 mPinnedZTranslationExtra = res.getDimensionPixelSize( 87 R.dimen.heads_up_pinned_elevation); 88 mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height); 89 } 90 91 /** 92 * Updates the state of all children in the hostview based on this algorithm. 93 */ resetViewStates(AmbientState ambientState)94 public void resetViewStates(AmbientState ambientState) { 95 // The state of the local variables are saved in an algorithmState to easily subdivide it 96 // into multiple phases. 97 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 98 99 // First we reset the view states to their default values. 100 resetChildViewStates(); 101 102 initAlgorithmState(mHostView, algorithmState, ambientState); 103 104 updatePositionsForState(algorithmState, ambientState); 105 106 updateZValuesForState(algorithmState, ambientState); 107 108 updateHeadsUpStates(algorithmState, ambientState); 109 updatePulsingStates(algorithmState, ambientState); 110 111 updateDimmedActivatedHideSensitive(ambientState, algorithmState); 112 updateClipping(algorithmState, ambientState); 113 updateSpeedBumpState(algorithmState, ambientState); 114 updateShelfState(ambientState); 115 getNotificationChildrenStates(algorithmState, ambientState); 116 } 117 resetChildViewStates()118 private void resetChildViewStates() { 119 int numChildren = mHostView.getChildCount(); 120 for (int i = 0; i < numChildren; i++) { 121 ExpandableView child = (ExpandableView) mHostView.getChildAt(i); 122 child.resetViewState(); 123 } 124 } 125 getNotificationChildrenStates(StackScrollAlgorithmState algorithmState, AmbientState ambientState)126 private void getNotificationChildrenStates(StackScrollAlgorithmState algorithmState, 127 AmbientState ambientState) { 128 int childCount = algorithmState.visibleChildren.size(); 129 for (int i = 0; i < childCount; i++) { 130 ExpandableView v = algorithmState.visibleChildren.get(i); 131 if (v instanceof ExpandableNotificationRow) { 132 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 133 row.updateChildrenStates(ambientState); 134 } 135 } 136 } 137 updateSpeedBumpState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)138 private void updateSpeedBumpState(StackScrollAlgorithmState algorithmState, 139 AmbientState ambientState) { 140 int childCount = algorithmState.visibleChildren.size(); 141 int belowSpeedBump = ambientState.getSpeedBumpIndex(); 142 for (int i = 0; i < childCount; i++) { 143 ExpandableView child = algorithmState.visibleChildren.get(i); 144 ExpandableViewState childViewState = child.getViewState(); 145 146 // The speed bump can also be gone, so equality needs to be taken when comparing 147 // indices. 148 childViewState.belowSpeedBump = i >= belowSpeedBump; 149 } 150 151 } 152 updateShelfState(AmbientState ambientState)153 private void updateShelfState(AmbientState ambientState) { 154 NotificationShelf shelf = ambientState.getShelf(); 155 if (shelf != null) { 156 shelf.updateState(ambientState); 157 } 158 } 159 updateClipping(StackScrollAlgorithmState algorithmState, AmbientState ambientState)160 private void updateClipping(StackScrollAlgorithmState algorithmState, 161 AmbientState ambientState) { 162 float drawStart = !ambientState.isOnKeyguard() ? ambientState.getTopPadding() 163 + ambientState.getStackTranslation() + ambientState.getExpandAnimationTopChange() 164 : 0; 165 float clipStart = 0; 166 int childCount = algorithmState.visibleChildren.size(); 167 boolean firstHeadsUp = true; 168 for (int i = 0; i < childCount; i++) { 169 ExpandableView child = algorithmState.visibleChildren.get(i); 170 ExpandableViewState state = child.getViewState(); 171 if (!child.mustStayOnScreen() || state.headsUpIsVisible) { 172 clipStart = Math.max(drawStart, clipStart); 173 } 174 float newYTranslation = state.yTranslation; 175 float newHeight = state.height; 176 float newNotificationEnd = newYTranslation + newHeight; 177 boolean isHeadsUp = (child instanceof ExpandableNotificationRow) 178 && ((ExpandableNotificationRow) child).isPinned(); 179 if (mClipNotificationScrollToTop 180 && (!state.inShelf || (isHeadsUp && !firstHeadsUp)) 181 && newYTranslation < clipStart) { 182 // The previous view is overlapping on top, clip! 183 float overlapAmount = clipStart - newYTranslation; 184 state.clipTopAmount = (int) overlapAmount; 185 } else { 186 state.clipTopAmount = 0; 187 } 188 if (isHeadsUp) { 189 firstHeadsUp = false; 190 } 191 if (!child.isTransparent()) { 192 // Only update the previous values if we are not transparent, 193 // otherwise we would clip to a transparent view. 194 clipStart = Math.max(clipStart, isHeadsUp ? newYTranslation : newNotificationEnd); 195 } 196 } 197 } 198 199 /** 200 * Updates the dimmed, activated and hiding sensitive states of the children. 201 */ updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollAlgorithmState algorithmState)202 private void updateDimmedActivatedHideSensitive(AmbientState ambientState, 203 StackScrollAlgorithmState algorithmState) { 204 boolean dimmed = ambientState.isDimmed(); 205 boolean hideSensitive = ambientState.isHideSensitive(); 206 View activatedChild = ambientState.getActivatedChild(); 207 int childCount = algorithmState.visibleChildren.size(); 208 for (int i = 0; i < childCount; i++) { 209 ExpandableView child = algorithmState.visibleChildren.get(i); 210 ExpandableViewState childViewState = child.getViewState(); 211 childViewState.dimmed = dimmed; 212 childViewState.hideSensitive = hideSensitive; 213 boolean isActivatedChild = activatedChild == child; 214 if (dimmed && isActivatedChild) { 215 childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements(); 216 } 217 } 218 } 219 220 /** 221 * Initialize the algorithm state like updating the visible children. 222 */ initAlgorithmState(ViewGroup hostView, StackScrollAlgorithmState state, AmbientState ambientState)223 private void initAlgorithmState(ViewGroup hostView, StackScrollAlgorithmState state, 224 AmbientState ambientState) { 225 float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); 226 227 int scrollY = ambientState.getScrollY(); 228 229 // Due to the overScroller, the stackscroller can have negative scroll state. This is 230 // already accounted for by the top padding and doesn't need an additional adaption 231 scrollY = Math.max(0, scrollY); 232 state.scrollY = (int) (scrollY + bottomOverScroll); 233 234 if (ANCHOR_SCROLLING) { 235 state.anchorViewY = (int) (ambientState.getAnchorViewY() - bottomOverScroll); 236 } 237 238 //now init the visible children and update paddings 239 int childCount = hostView.getChildCount(); 240 state.visibleChildren.clear(); 241 state.visibleChildren.ensureCapacity(childCount); 242 state.paddingMap.clear(); 243 int notGoneIndex = 0; 244 ExpandableView lastView = null; 245 int firstHiddenIndex = ambientState.isDozing() 246 ? (ambientState.hasPulsingNotifications() ? 1 : 0) 247 : childCount; 248 249 // The goal here is to fill the padding map, by iterating over how much padding each child 250 // needs. The map is thereby reused, by first filling it with the padding amount and when 251 // iterating over it again, it's filled with the actual resolved value. 252 253 for (int i = 0; i < childCount; i++) { 254 if (ANCHOR_SCROLLING) { 255 if (i == ambientState.getAnchorViewIndex()) { 256 state.anchorViewIndex = state.visibleChildren.size(); 257 } 258 } 259 ExpandableView v = (ExpandableView) hostView.getChildAt(i); 260 if (v.getVisibility() != View.GONE) { 261 if (v == ambientState.getShelf()) { 262 continue; 263 } 264 if (i >= firstHiddenIndex) { 265 // we need normal padding now, to be in sync with what the stack calculates 266 lastView = null; 267 } 268 notGoneIndex = updateNotGoneIndex(state, notGoneIndex, v); 269 float increasedPadding = v.getIncreasedPaddingAmount(); 270 if (increasedPadding != 0.0f) { 271 state.paddingMap.put(v, increasedPadding); 272 if (lastView != null) { 273 Float prevValue = state.paddingMap.get(lastView); 274 float newValue = getPaddingForValue(increasedPadding); 275 if (prevValue != null) { 276 float prevPadding = getPaddingForValue(prevValue); 277 if (increasedPadding > 0) { 278 newValue = NotificationUtils.interpolate( 279 prevPadding, 280 newValue, 281 increasedPadding); 282 } else if (prevValue > 0) { 283 newValue = NotificationUtils.interpolate( 284 newValue, 285 prevPadding, 286 prevValue); 287 } 288 } 289 state.paddingMap.put(lastView, newValue); 290 } 291 } else if (lastView != null) { 292 293 // Let's now resolve the value to an actual padding 294 float newValue = getPaddingForValue(state.paddingMap.get(lastView)); 295 state.paddingMap.put(lastView, newValue); 296 } 297 if (v instanceof ExpandableNotificationRow) { 298 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 299 300 // handle the notgoneIndex for the children as well 301 List<ExpandableNotificationRow> children = row.getAttachedChildren(); 302 if (row.isSummaryWithChildren() && children != null) { 303 for (ExpandableNotificationRow childRow : children) { 304 if (childRow.getVisibility() != View.GONE) { 305 ExpandableViewState childState = childRow.getViewState(); 306 childState.notGoneIndex = notGoneIndex; 307 notGoneIndex++; 308 } 309 } 310 } 311 } 312 lastView = v; 313 } 314 } 315 ExpandableNotificationRow expandingNotification = ambientState.getExpandingNotification(); 316 state.indexOfExpandingNotification = expandingNotification != null 317 ? expandingNotification.isChildInGroup() 318 ? state.visibleChildren.indexOf(expandingNotification.getNotificationParent()) 319 : state.visibleChildren.indexOf(expandingNotification) 320 : -1; 321 } 322 getPaddingForValue(Float increasedPadding)323 private float getPaddingForValue(Float increasedPadding) { 324 if (increasedPadding == null) { 325 return mPaddingBetweenElements; 326 } else if (increasedPadding >= 0.0f) { 327 return NotificationUtils.interpolate( 328 mPaddingBetweenElements, 329 mIncreasedPaddingBetweenElements, 330 increasedPadding); 331 } else { 332 return NotificationUtils.interpolate( 333 0, 334 mPaddingBetweenElements, 335 1.0f + increasedPadding); 336 } 337 } 338 updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v)339 private int updateNotGoneIndex(StackScrollAlgorithmState state, int notGoneIndex, 340 ExpandableView v) { 341 ExpandableViewState viewState = v.getViewState(); 342 viewState.notGoneIndex = notGoneIndex; 343 state.visibleChildren.add(v); 344 notGoneIndex++; 345 return notGoneIndex; 346 } 347 348 /** 349 * Determine the positions for the views. This is the main part of the algorithm. 350 * 351 * @param algorithmState The state in which the current pass of the algorithm is currently in 352 * @param ambientState The current ambient state 353 */ updatePositionsForState(StackScrollAlgorithmState algorithmState, AmbientState ambientState)354 private void updatePositionsForState(StackScrollAlgorithmState algorithmState, 355 AmbientState ambientState) { 356 if (ANCHOR_SCROLLING) { 357 float currentYPosition = algorithmState.anchorViewY; 358 int childCount = algorithmState.visibleChildren.size(); 359 for (int i = algorithmState.anchorViewIndex; i < childCount; i++) { 360 currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition, 361 false /* reverse */); 362 } 363 currentYPosition = algorithmState.anchorViewY; 364 for (int i = algorithmState.anchorViewIndex - 1; i >= 0; i--) { 365 currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition, 366 true /* reverse */); 367 } 368 } else { 369 // The y coordinate of the current child. 370 float currentYPosition = -algorithmState.scrollY; 371 int childCount = algorithmState.visibleChildren.size(); 372 for (int i = 0; i < childCount; i++) { 373 currentYPosition = updateChild(i, algorithmState, ambientState, currentYPosition, 374 false /* reverse */); 375 } 376 } 377 } 378 379 /** 380 * Populates the {@link ExpandableViewState} for a single child. 381 * 382 * @param i The index of the child in 383 * {@link StackScrollAlgorithmState#visibleChildren}. 384 * @param algorithmState The overall output state of the algorithm. 385 * @param ambientState The input state provided to the algorithm. 386 * @param currentYPosition The Y position of the current pass of the algorithm. For a forward 387 * pass, this should be the top of the child; for a reverse pass, the 388 * bottom of the child. 389 * @param reverse Whether we're laying out children in the reverse direction (Y 390 * positions 391 * decreasing) instead of the forward direction (Y positions 392 * increasing). 393 * @return The Y position after laying out the child. This will be the {@code currentYPosition} 394 * for the next call to this method, after adjusting for any gaps between children. 395 */ updateChild( int i, StackScrollAlgorithmState algorithmState, AmbientState ambientState, float currentYPosition, boolean reverse)396 protected float updateChild( 397 int i, 398 StackScrollAlgorithmState algorithmState, 399 AmbientState ambientState, 400 float currentYPosition, 401 boolean reverse) { 402 ExpandableView child = algorithmState.visibleChildren.get(i); 403 ExpandableView previousChild = i > 0 ? algorithmState.visibleChildren.get(i - 1) : null; 404 final boolean applyGapHeight = 405 childNeedsGapHeight( 406 ambientState.getSectionProvider(), algorithmState.anchorViewIndex, i, 407 child, previousChild); 408 ExpandableViewState childViewState = child.getViewState(); 409 childViewState.location = ExpandableViewState.LOCATION_UNKNOWN; 410 411 if (applyGapHeight && !reverse) { 412 currentYPosition += mGapHeight; 413 } 414 415 int paddingAfterChild = getPaddingAfterChild(algorithmState, child); 416 int childHeight = getMaxAllowedChildHeight(child); 417 if (reverse) { 418 childViewState.yTranslation = currentYPosition - (childHeight + paddingAfterChild); 419 if (currentYPosition <= 0) { 420 childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; 421 } 422 } else { 423 childViewState.yTranslation = currentYPosition; 424 } 425 boolean isFooterView = child instanceof FooterView; 426 boolean isEmptyShadeView = child instanceof EmptyShadeView; 427 428 childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; 429 float inset = ambientState.getTopPadding() + ambientState.getStackTranslation(); 430 if (i <= algorithmState.getIndexOfExpandingNotification()) { 431 inset += ambientState.getExpandAnimationTopChange(); 432 } 433 if (child.mustStayOnScreen() && childViewState.yTranslation >= 0) { 434 // Even if we're not scrolled away we're in view and we're also not in the 435 // shelf. We can relax the constraints and let us scroll off the top! 436 float end = childViewState.yTranslation + childViewState.height + inset; 437 childViewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation(); 438 } 439 if (isFooterView) { 440 childViewState.yTranslation = Math.min(childViewState.yTranslation, 441 ambientState.getInnerHeight() - childHeight); 442 } else if (isEmptyShadeView) { 443 childViewState.yTranslation = ambientState.getInnerHeight() - childHeight 444 + ambientState.getStackTranslation() * 0.25f; 445 } else if (child != ambientState.getTrackedHeadsUpRow()) { 446 clampPositionToShelf(child, childViewState, ambientState); 447 } 448 449 if (reverse) { 450 currentYPosition = childViewState.yTranslation; 451 if (applyGapHeight) { 452 currentYPosition -= mGapHeight; 453 } 454 } else { 455 currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild; 456 if (currentYPosition <= 0) { 457 childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; 458 } 459 } 460 if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) { 461 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 462 } 463 464 childViewState.yTranslation += inset; 465 return currentYPosition; 466 } 467 468 /** 469 * Get the gap height needed for before a view 470 * 471 * @param sectionProvider the sectionProvider used to understand the sections 472 * @param anchorViewIndex the anchorView index when anchor scrolling, can be 0 if not 473 * @param visibleIndex the visible index of this view in the list 474 * @param child the child asked about 475 * @param previousChild the child right before it or null if none 476 * @return the size of the gap needed or 0 if none is needed 477 */ 478 public float getGapHeightForChild( 479 SectionProvider sectionProvider, 480 int anchorViewIndex, 481 int visibleIndex, 482 View child, 483 View previousChild) { 484 485 if (childNeedsGapHeight(sectionProvider, anchorViewIndex, visibleIndex, child, 486 previousChild)) { 487 return mGapHeight; 488 } else { 489 return 0; 490 } 491 } 492 493 /** 494 * Does a given child need a gap, i.e spacing before a view? 495 * 496 * @param sectionProvider the sectionProvider used to understand the sections 497 * @param anchorViewIndex the anchorView index when anchor scrolling, can be 0 if not 498 * @param visibleIndex the visible index of this view in the list 499 * @param child the child asked about 500 * @param previousChild the child right before it or null if none 501 * @return if the child needs a gap height 502 */ 503 private boolean childNeedsGapHeight( 504 SectionProvider sectionProvider, 505 int anchorViewIndex, 506 int visibleIndex, 507 View child, 508 View previousChild) { 509 510 boolean needsGapHeight = sectionProvider.beginsSection(child, previousChild) 511 && visibleIndex > 0; 512 if (ANCHOR_SCROLLING) { 513 needsGapHeight &= visibleIndex != anchorViewIndex; 514 } 515 return needsGapHeight; 516 } 517 518 protected int getPaddingAfterChild(StackScrollAlgorithmState algorithmState, 519 ExpandableView child) { 520 return algorithmState.getPaddingAfterChild(child); 521 } 522 523 private void updatePulsingStates(StackScrollAlgorithmState algorithmState, 524 AmbientState ambientState) { 525 int childCount = algorithmState.visibleChildren.size(); 526 for (int i = 0; i < childCount; i++) { 527 View child = algorithmState.visibleChildren.get(i); 528 if (!(child instanceof ExpandableNotificationRow)) { 529 continue; 530 } 531 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 532 if (!row.showingPulsing() || (i == 0 && ambientState.isPulseExpanding())) { 533 continue; 534 } 535 ExpandableViewState viewState = row.getViewState(); 536 viewState.hidden = false; 537 } 538 } 539 540 private void updateHeadsUpStates(StackScrollAlgorithmState algorithmState, 541 AmbientState ambientState) { 542 int childCount = algorithmState.visibleChildren.size(); 543 544 // Move the tracked heads up into position during the appear animation, by interpolating 545 // between the HUN inset (where it will appear as a HUN) and the end position in the shade 546 ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow(); 547 if (trackedHeadsUpRow != null) { 548 ExpandableViewState childState = trackedHeadsUpRow.getViewState(); 549 if (childState != null) { 550 float endPosition = childState.yTranslation - ambientState.getStackTranslation(); 551 childState.yTranslation = MathUtils.lerp( 552 mHeadsUpInset, endPosition, ambientState.getAppearFraction()); 553 } 554 } 555 556 ExpandableNotificationRow topHeadsUpEntry = null; 557 for (int i = 0; i < childCount; i++) { 558 View child = algorithmState.visibleChildren.get(i); 559 if (!(child instanceof ExpandableNotificationRow)) { 560 continue; 561 } 562 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 563 if (!row.isHeadsUp()) { 564 continue; 565 } 566 ExpandableViewState childState = row.getViewState(); 567 if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) { 568 topHeadsUpEntry = row; 569 childState.location = ExpandableViewState.LOCATION_FIRST_HUN; 570 } 571 boolean isTopEntry = topHeadsUpEntry == row; 572 float unmodifiedEndLocation = childState.yTranslation + childState.height; 573 if (mIsExpanded) { 574 if (row.mustStayOnScreen() && !childState.headsUpIsVisible 575 && !row.showingPulsing()) { 576 // Ensure that the heads up is always visible even when scrolled off 577 clampHunToTop(ambientState, row, childState); 578 if (isTopEntry && row.isAboveShelf()) { 579 // the first hun can't get off screen. 580 clampHunToMaxTranslation(ambientState, row, childState); 581 childState.hidden = false; 582 } 583 } 584 } 585 if (row.isPinned()) { 586 childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset); 587 childState.height = Math.max(row.getIntrinsicHeight(), childState.height); 588 childState.hidden = false; 589 ExpandableViewState topState = 590 topHeadsUpEntry == null ? null : topHeadsUpEntry.getViewState(); 591 if (topState != null && !isTopEntry && (!mIsExpanded 592 || unmodifiedEndLocation > topState.yTranslation + topState.height)) { 593 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 594 // the top most z-position 595 childState.height = row.getIntrinsicHeight(); 596 childState.yTranslation = Math.min(topState.yTranslation + topState.height 597 - childState.height, childState.yTranslation); 598 } 599 600 // heads up notification show and this row is the top entry of heads up 601 // notifications. i.e. this row should be the only one row that has input field 602 // To check if the row need to do translation according to scroll Y 603 // heads up show full of row's content and any scroll y indicate that the 604 // translationY need to move up the HUN. 605 // TODO: fix this check for anchor scrolling. 606 if (!mIsExpanded && isTopEntry && ambientState.getScrollY() > 0) { 607 childState.yTranslation -= ambientState.getScrollY(); 608 } 609 } 610 if (row.isHeadsUpAnimatingAway()) { 611 childState.hidden = false; 612 } 613 } 614 } 615 616 private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, 617 ExpandableViewState childState) { 618 float newTranslation = Math.max(ambientState.getTopPadding() 619 + ambientState.getStackTranslation(), childState.yTranslation); 620 childState.height = (int) Math.max(childState.height - (newTranslation 621 - childState.yTranslation), row.getCollapsedHeight()); 622 childState.yTranslation = newTranslation; 623 } 624 625 private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, 626 ExpandableViewState childState) { 627 float newTranslation; 628 float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation(); 629 float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding() 630 + ambientState.getStackTranslation(); 631 maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition); 632 float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight(); 633 newTranslation = Math.min(childState.yTranslation, bottomPosition); 634 childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation 635 - newTranslation); 636 childState.yTranslation = newTranslation; 637 } 638 639 /** 640 * Clamp the height of the child down such that its end is at most on the beginning of 641 * the shelf. 642 * 643 * @param childViewState the view state of the child 644 * @param ambientState the ambient state 645 */ 646 private void clampPositionToShelf(ExpandableView child, 647 ExpandableViewState childViewState, 648 AmbientState ambientState) { 649 if (ambientState.getShelf() == null) { 650 return; 651 } 652 653 ExpandableNotificationRow trackedHeadsUpRow = ambientState.getTrackedHeadsUpRow(); 654 boolean isBeforeTrackedHeadsUp = trackedHeadsUpRow != null 655 && mHostView.indexOfChild(child) < mHostView.indexOfChild(trackedHeadsUpRow); 656 657 int shelfStart = ambientState.getInnerHeight() 658 - ambientState.getShelf().getIntrinsicHeight(); 659 if (ambientState.isAppearing() && !child.isAboveShelf() && !isBeforeTrackedHeadsUp) { 660 // Don't show none heads-up notifications while in appearing phase. 661 childViewState.yTranslation = Math.max(childViewState.yTranslation, shelfStart); 662 } 663 childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart); 664 if (childViewState.yTranslation >= shelfStart) { 665 childViewState.hidden = !child.isExpandAnimationRunning() && !child.hasExpandingChild(); 666 childViewState.inShelf = true; 667 childViewState.headsUpIsVisible = false; 668 } 669 } 670 671 protected int getMaxAllowedChildHeight(View child) { 672 if (child instanceof ExpandableView) { 673 ExpandableView expandableView = (ExpandableView) child; 674 return expandableView.getIntrinsicHeight(); 675 } 676 return child == null ? mCollapsedSize : child.getHeight(); 677 } 678 679 /** 680 * Calculate the Z positions for all children based on the number of items in both stacks and 681 * save it in the resultState 682 * 683 * @param algorithmState The state in which the current pass of the algorithm is currently in 684 * @param ambientState The ambient state of the algorithm 685 */ 686 private void updateZValuesForState(StackScrollAlgorithmState algorithmState, 687 AmbientState ambientState) { 688 int childCount = algorithmState.visibleChildren.size(); 689 float childrenOnTop = 0.0f; 690 for (int i = childCount - 1; i >= 0; i--) { 691 childrenOnTop = updateChildZValue(i, childrenOnTop, 692 algorithmState, ambientState); 693 } 694 } 695 updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, AmbientState ambientState)696 protected float updateChildZValue(int i, float childrenOnTop, 697 StackScrollAlgorithmState algorithmState, 698 AmbientState ambientState) { 699 ExpandableView child = algorithmState.visibleChildren.get(i); 700 ExpandableViewState childViewState = child.getViewState(); 701 int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); 702 float baseZ = ambientState.getBaseZHeight(); 703 if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible 704 && !ambientState.isDozingAndNotPulsing(child) 705 && childViewState.yTranslation < ambientState.getTopPadding() 706 + ambientState.getStackTranslation()) { 707 if (childrenOnTop != 0.0f) { 708 childrenOnTop++; 709 } else { 710 float overlap = ambientState.getTopPadding() 711 + ambientState.getStackTranslation() - childViewState.yTranslation; 712 childrenOnTop += Math.min(1.0f, overlap / childViewState.height); 713 } 714 childViewState.zTranslation = baseZ 715 + childrenOnTop * zDistanceBetweenElements; 716 } else if (child == ambientState.getTrackedHeadsUpRow() 717 || (i == 0 && (child.isAboveShelf() || child.showingPulsing()))) { 718 // In case this is a new view that has never been measured before, we don't want to 719 // elevate if we are currently expanded more then the notification 720 int shelfHeight = ambientState.getShelf() == null ? 0 : 721 ambientState.getShelf().getIntrinsicHeight(); 722 float shelfStart = ambientState.getInnerHeight() 723 - shelfHeight + ambientState.getTopPadding() 724 + ambientState.getStackTranslation(); 725 float notificationEnd = childViewState.yTranslation + child.getIntrinsicHeight() 726 + mPaddingBetweenElements; 727 if (shelfStart > notificationEnd) { 728 childViewState.zTranslation = baseZ; 729 } else { 730 float factor = (notificationEnd - shelfStart) / shelfHeight; 731 factor = Math.min(factor, 1.0f); 732 childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements; 733 } 734 } else { 735 childViewState.zTranslation = baseZ; 736 } 737 738 // We need to scrim the notification more from its surrounding content when we are pinned, 739 // and we therefore elevate it higher. 740 // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when 741 // expanding after which we have a normal elevation again. 742 childViewState.zTranslation += (1.0f - child.getHeaderVisibleAmount()) 743 * mPinnedZTranslationExtra; 744 return childrenOnTop; 745 } 746 setIsExpanded(boolean isExpanded)747 public void setIsExpanded(boolean isExpanded) { 748 this.mIsExpanded = isExpanded; 749 } 750 751 public class StackScrollAlgorithmState { 752 753 /** 754 * The scroll position of the algorithm (absolute scrolling). 755 */ 756 public int scrollY; 757 758 /** The index of the anchor view (anchor scrolling). */ 759 public int anchorViewIndex; 760 761 /** 762 * The Y position, relative to the top of the screen, of the anchor view (anchor scrolling). 763 */ 764 public int anchorViewY; 765 766 /** 767 * The children from the host view which are not gone. 768 */ 769 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 770 771 /** 772 * The padding after each child measured in pixels. 773 */ 774 public final HashMap<ExpandableView, Float> paddingMap = new HashMap<>(); 775 private int indexOfExpandingNotification; 776 getPaddingAfterChild(ExpandableView child)777 public int getPaddingAfterChild(ExpandableView child) { 778 Float padding = paddingMap.get(child); 779 if (padding == null) { 780 // Should only happen for the last view 781 return mPaddingBetweenElements; 782 } 783 return (int) padding.floatValue(); 784 } 785 getIndexOfExpandingNotification()786 public int getIndexOfExpandingNotification() { 787 return indexOfExpandingNotification; 788 } 789 } 790 791 /** 792 * Interface for telling the SSA when a new notification section begins (so it can add in 793 * appropriate margins). 794 */ 795 public interface SectionProvider { 796 /** 797 * True if this view starts a new "section" of notifications, such as the gentle 798 * notifications section. False if sections are not enabled. 799 */ 800 boolean beginsSection(@NonNull View view, @Nullable View previous); 801 } 802 } 803