StackScrollAlgorithm.java revision ebf423466cff0b2db08d322dbf9772e9b1a3bbf1
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package com.android.systemui.statusbar.stack; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.util.Log; 22import android.view.View; 23import android.view.ViewGroup; 24import com.android.systemui.R; 25import com.android.systemui.statusbar.DismissView; 26import com.android.systemui.statusbar.EmptyShadeView; 27import com.android.systemui.statusbar.ExpandableNotificationRow; 28import com.android.systemui.statusbar.ExpandableView; 29import com.android.systemui.statusbar.NotificationShelf; 30import com.android.systemui.statusbar.notification.NotificationUtils; 31 32import java.util.ArrayList; 33import java.util.HashMap; 34import java.util.List; 35 36/** 37 * The Algorithm of the {@link com.android.systemui.statusbar.stack 38 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar 39 * .stack.StackScrollState} 40 */ 41public class StackScrollAlgorithm { 42 43 private static final String LOG_TAG = "StackScrollAlgorithm"; 44 45 private int mPaddingBetweenElements; 46 private int mIncreasedPaddingBetweenElements; 47 private int mCollapsedSize; 48 49 private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); 50 private boolean mIsExpanded; 51 private boolean mClipNotificationScrollToTop; 52 private int mStatusBarHeight; 53 54 public StackScrollAlgorithm(Context context) { 55 initView(context); 56 } 57 58 public void initView(Context context) { 59 initConstants(context); 60 } 61 62 private void initConstants(Context context) { 63 Resources res = context.getResources(); 64 mPaddingBetweenElements = res.getDimensionPixelSize( 65 R.dimen.notification_divider_height); 66 mIncreasedPaddingBetweenElements = 67 res.getDimensionPixelSize(R.dimen.notification_divider_height_increased); 68 mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height); 69 mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height); 70 mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop); 71 } 72 73 public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) { 74 // The state of the local variables are saved in an algorithmState to easily subdivide it 75 // into multiple phases. 76 StackScrollAlgorithmState algorithmState = mTempAlgorithmState; 77 78 // First we reset the view states to their default values. 79 resultState.resetViewStates(); 80 81 initAlgorithmState(resultState, algorithmState, ambientState); 82 83 updatePositionsForState(resultState, algorithmState, ambientState); 84 85 updateZValuesForState(resultState, algorithmState, ambientState); 86 87 updateHeadsUpStates(resultState, algorithmState, ambientState); 88 89 handleDraggedViews(ambientState, resultState, algorithmState); 90 updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState); 91 updateClipping(resultState, algorithmState, ambientState); 92 updateSpeedBumpState(resultState, algorithmState, ambientState); 93 updateShelfState(resultState, ambientState); 94 getNotificationChildrenStates(resultState, algorithmState); 95 } 96 97 private void getNotificationChildrenStates(StackScrollState resultState, 98 StackScrollAlgorithmState algorithmState) { 99 int childCount = algorithmState.visibleChildren.size(); 100 for (int i = 0; i < childCount; i++) { 101 ExpandableView v = algorithmState.visibleChildren.get(i); 102 if (v instanceof ExpandableNotificationRow) { 103 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 104 row.getChildrenStates(resultState); 105 } 106 } 107 } 108 109 private void updateSpeedBumpState(StackScrollState resultState, 110 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 111 int childCount = algorithmState.visibleChildren.size(); 112 int belowSpeedBump = ambientState.getSpeedBumpIndex(); 113 for (int i = 0; i < childCount; i++) { 114 View child = algorithmState.visibleChildren.get(i); 115 ExpandableViewState childViewState = resultState.getViewStateForView(child); 116 117 // The speed bump can also be gone, so equality needs to be taken when comparing 118 // indices. 119 childViewState.belowSpeedBump = i >= belowSpeedBump; 120 } 121 122 } 123 private void updateShelfState(StackScrollState resultState, AmbientState ambientState) { 124 NotificationShelf shelf = ambientState.getShelf(); 125 shelf.updateState(resultState, ambientState); 126 } 127 128 private void updateClipping(StackScrollState resultState, 129 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 130 float drawStart = !ambientState.isOnKeyguard() ? ambientState.getTopPadding() 131 + ambientState.getStackTranslation() : 0; 132 float previousNotificationEnd = 0; 133 float previousNotificationStart = 0; 134 int childCount = algorithmState.visibleChildren.size(); 135 for (int i = 0; i < childCount; i++) { 136 ExpandableView child = algorithmState.visibleChildren.get(i); 137 ExpandableViewState state = resultState.getViewStateForView(child); 138 if (!child.mustStayOnScreen()) { 139 previousNotificationEnd = Math.max(drawStart, previousNotificationEnd); 140 previousNotificationStart = Math.max(drawStart, previousNotificationStart); 141 } 142 float newYTranslation = state.yTranslation; 143 float newHeight = state.height; 144 float newNotificationEnd = newYTranslation + newHeight; 145 boolean isHeadsUp = (child instanceof ExpandableNotificationRow) 146 && ((ExpandableNotificationRow) child).isPinned(); 147 if (mClipNotificationScrollToTop 148 && !state.inShelf && newYTranslation < previousNotificationEnd 149 && (!isHeadsUp || ambientState.isShadeExpanded())) { 150 // The previous view is overlapping on top, clip! 151 float overlapAmount = previousNotificationEnd - newYTranslation; 152 state.clipTopAmount = (int) overlapAmount; 153 } else { 154 state.clipTopAmount = 0; 155 } 156 157 if (!child.isTransparent()) { 158 // Only update the previous values if we are not transparent, 159 // otherwise we would clip to a transparent view. 160 previousNotificationEnd = newNotificationEnd; 161 previousNotificationStart = newYTranslation; 162 } 163 } 164 } 165 166 public static boolean canChildBeDismissed(View v) { 167 if (!(v instanceof ExpandableNotificationRow)) { 168 return false; 169 } 170 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 171 if (row.areGutsExposed()) { 172 return false; 173 } 174 return row.canViewBeDismissed(); 175 } 176 177 /** 178 * Updates the dimmed, activated and hiding sensitive states of the children. 179 */ 180 private void updateDimmedActivatedHideSensitive(AmbientState ambientState, 181 StackScrollState resultState, StackScrollAlgorithmState algorithmState) { 182 boolean dimmed = ambientState.isDimmed(); 183 boolean dark = ambientState.isDark(); 184 boolean hideSensitive = ambientState.isHideSensitive(); 185 View activatedChild = ambientState.getActivatedChild(); 186 int childCount = algorithmState.visibleChildren.size(); 187 for (int i = 0; i < childCount; i++) { 188 View child = algorithmState.visibleChildren.get(i); 189 ExpandableViewState childViewState = resultState.getViewStateForView(child); 190 childViewState.dimmed = dimmed; 191 childViewState.dark = dark; 192 childViewState.hideSensitive = hideSensitive; 193 boolean isActivatedChild = activatedChild == child; 194 if (dimmed && isActivatedChild) { 195 childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements(); 196 } 197 } 198 } 199 200 /** 201 * Handle the special state when views are being dragged 202 */ 203 private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState, 204 StackScrollAlgorithmState algorithmState) { 205 ArrayList<View> draggedViews = ambientState.getDraggedViews(); 206 for (View draggedView : draggedViews) { 207 int childIndex = algorithmState.visibleChildren.indexOf(draggedView); 208 if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) { 209 View nextChild = algorithmState.visibleChildren.get(childIndex + 1); 210 if (!draggedViews.contains(nextChild)) { 211 // only if the view is not dragged itself we modify its state to be fully 212 // visible 213 ExpandableViewState viewState = resultState.getViewStateForView( 214 nextChild); 215 // The child below the dragged one must be fully visible 216 if (ambientState.isShadeExpanded()) { 217 viewState.shadowAlpha = 1; 218 viewState.hidden = false; 219 } 220 } 221 222 // Lets set the alpha to the one it currently has, as its currently being dragged 223 ExpandableViewState viewState = resultState.getViewStateForView(draggedView); 224 // The dragged child should keep the set alpha 225 viewState.alpha = draggedView.getAlpha(); 226 } 227 } 228 } 229 230 /** 231 * Initialize the algorithm state like updating the visible children. 232 */ 233 private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state, 234 AmbientState ambientState) { 235 float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); 236 237 int scrollY = ambientState.getScrollY(); 238 239 // Due to the overScroller, the stackscroller can have negative scroll state. This is 240 // already accounted for by the top padding and doesn't need an additional adaption 241 scrollY = Math.max(0, scrollY); 242 state.scrollY = (int) (scrollY + bottomOverScroll); 243 244 //now init the visible children and update paddings 245 ViewGroup hostView = resultState.getHostView(); 246 int childCount = hostView.getChildCount(); 247 state.visibleChildren.clear(); 248 state.visibleChildren.ensureCapacity(childCount); 249 state.paddingMap.clear(); 250 int notGoneIndex = 0; 251 ExpandableView lastView = null; 252 for (int i = 0; i < childCount; i++) { 253 ExpandableView v = (ExpandableView) hostView.getChildAt(i); 254 if (v.getVisibility() != View.GONE) { 255 if (v == ambientState.getShelf()) { 256 continue; 257 } 258 notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v); 259 float increasedPadding = v.getIncreasedPaddingAmount();; 260 if (increasedPadding != 0.0f) { 261 state.paddingMap.put(v, increasedPadding); 262 if (lastView != null) { 263 Float prevValue = state.paddingMap.get(lastView); 264 float newValue = getPaddingForValue(increasedPadding); 265 if (prevValue != null) { 266 float prevPadding = getPaddingForValue(prevValue); 267 if (increasedPadding > 0) { 268 newValue = NotificationUtils.interpolate( 269 prevPadding, 270 newValue, 271 increasedPadding); 272 } else if (prevValue > 0) { 273 newValue = NotificationUtils.interpolate( 274 newValue, 275 prevPadding, 276 prevValue); 277 } 278 } 279 state.paddingMap.put(lastView, newValue); 280 } 281 } else if (lastView != null) { 282 float newValue = getPaddingForValue(state.paddingMap.get(lastView)); 283 state.paddingMap.put(lastView, newValue); 284 } 285 if (v instanceof ExpandableNotificationRow) { 286 ExpandableNotificationRow row = (ExpandableNotificationRow) v; 287 288 // handle the notgoneIndex for the children as well 289 List<ExpandableNotificationRow> children = 290 row.getNotificationChildren(); 291 if (row.isSummaryWithChildren() && children != null) { 292 for (ExpandableNotificationRow childRow : children) { 293 if (childRow.getVisibility() != View.GONE) { 294 ExpandableViewState childState 295 = resultState.getViewStateForView(childRow); 296 childState.notGoneIndex = notGoneIndex; 297 notGoneIndex++; 298 } 299 } 300 } 301 } 302 lastView = v; 303 } 304 } 305 } 306 307 private float getPaddingForValue(Float increasedPadding) { 308 if (increasedPadding == null) { 309 return mPaddingBetweenElements; 310 } else if (increasedPadding >= 0.0f) { 311 return NotificationUtils.interpolate( 312 mPaddingBetweenElements, 313 mIncreasedPaddingBetweenElements, 314 increasedPadding); 315 } else { 316 return NotificationUtils.interpolate( 317 0, 318 mPaddingBetweenElements, 319 1.0f + increasedPadding); 320 } 321 } 322 323 private int updateNotGoneIndex(StackScrollState resultState, 324 StackScrollAlgorithmState state, int notGoneIndex, 325 ExpandableView v) { 326 ExpandableViewState viewState = resultState.getViewStateForView(v); 327 viewState.notGoneIndex = notGoneIndex; 328 state.visibleChildren.add(v); 329 notGoneIndex++; 330 return notGoneIndex; 331 } 332 333 /** 334 * Determine the positions for the views. This is the main part of the algorithm. 335 * 336 * @param resultState The result state to update if a change to the properties of a child occurs 337 * @param algorithmState The state in which the current pass of the algorithm is currently in 338 * @param ambientState The current ambient state 339 */ 340 private void updatePositionsForState(StackScrollState resultState, 341 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 342 343 // The y coordinate of the current child. 344 float currentYPosition = -algorithmState.scrollY; 345 int childCount = algorithmState.visibleChildren.size(); 346 for (int i = 0; i < childCount; i++) { 347 currentYPosition = updateChild(i, resultState, algorithmState, ambientState, 348 currentYPosition); 349 } 350 } 351 352 protected float updateChild(int i, StackScrollState resultState, 353 StackScrollAlgorithmState algorithmState, AmbientState ambientState, 354 float currentYPosition) { 355 ExpandableView child = algorithmState.visibleChildren.get(i); 356 ExpandableViewState childViewState = resultState.getViewStateForView(child); 357 childViewState.location = ExpandableViewState.LOCATION_UNKNOWN; 358 int paddingAfterChild = getPaddingAfterChild(algorithmState, child); 359 int childHeight = getMaxAllowedChildHeight(child); 360 childViewState.yTranslation = currentYPosition; 361 boolean isDismissView = child instanceof DismissView; 362 boolean isEmptyShadeView = child instanceof EmptyShadeView; 363 364 childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA; 365 if (isDismissView) { 366 childViewState.yTranslation = Math.min(childViewState.yTranslation, 367 ambientState.getInnerHeight() - childHeight); 368 } else if (isEmptyShadeView) { 369 childViewState.yTranslation = ambientState.getInnerHeight() - childHeight 370 + ambientState.getStackTranslation() * 0.25f; 371 } else { 372 clampPositionToShelf(childViewState, ambientState); 373 } 374 375 currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild; 376 if (currentYPosition <= 0) { 377 childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP; 378 } 379 if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) { 380 Log.wtf(LOG_TAG, "Failed to assign location for child " + i); 381 } 382 383 childViewState.yTranslation += ambientState.getTopPadding() 384 + ambientState.getStackTranslation(); 385 return currentYPosition; 386 } 387 388 protected int getPaddingAfterChild(StackScrollAlgorithmState algorithmState, 389 ExpandableView child) { 390 return algorithmState.getPaddingAfterChild(child); 391 } 392 393 private void updateHeadsUpStates(StackScrollState resultState, 394 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 395 int childCount = algorithmState.visibleChildren.size(); 396 ExpandableNotificationRow topHeadsUpEntry = null; 397 for (int i = 0; i < childCount; i++) { 398 View child = algorithmState.visibleChildren.get(i); 399 if (!(child instanceof ExpandableNotificationRow)) { 400 break; 401 } 402 ExpandableNotificationRow row = (ExpandableNotificationRow) child; 403 if (!row.isHeadsUp()) { 404 break; 405 } 406 ExpandableViewState childState = resultState.getViewStateForView(row); 407 if (topHeadsUpEntry == null) { 408 topHeadsUpEntry = row; 409 childState.location = ExpandableViewState.LOCATION_FIRST_HUN; 410 } 411 boolean isTopEntry = topHeadsUpEntry == row; 412 float unmodifiedEndLocation = childState.yTranslation + childState.height; 413 if (mIsExpanded) { 414 // Ensure that the heads up is always visible even when scrolled off 415 clampHunToTop(ambientState, row, childState); 416 if (i == 0 && ambientState.isAboveShelf(row)) { 417 // the first hun can't get off screen. 418 clampHunToMaxTranslation(ambientState, row, childState); 419 childState.hidden = false; 420 } 421 } 422 if (row.isPinned()) { 423 childState.yTranslation = Math.max(childState.yTranslation, 0); 424 childState.height = Math.max(row.getIntrinsicHeight(), childState.height); 425 childState.hidden = false; 426 ExpandableViewState topState = resultState.getViewStateForView(topHeadsUpEntry); 427 if (!isTopEntry && (!mIsExpanded 428 || unmodifiedEndLocation < topState.yTranslation + topState.height)) { 429 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 430 // the top most z-position 431 childState.height = row.getIntrinsicHeight(); 432 childState.yTranslation = topState.yTranslation + topState.height 433 - childState.height; 434 } 435 } 436 if (row.isHeadsUpAnimatingAway()) { 437 childState.hidden = false; 438 } 439 } 440 } 441 442 private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, 443 ExpandableViewState childState) { 444 float newTranslation = Math.max(ambientState.getTopPadding() 445 + ambientState.getStackTranslation(), childState.yTranslation); 446 childState.height = (int) Math.max(childState.height - (newTranslation 447 - childState.yTranslation), row.getCollapsedHeight()); 448 childState.yTranslation = newTranslation; 449 } 450 451 private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, 452 ExpandableViewState childState) { 453 float newTranslation; 454 float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation(); 455 float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding() 456 + ambientState.getStackTranslation(); 457 maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition); 458 float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight(); 459 newTranslation = Math.min(childState.yTranslation, bottomPosition); 460 childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation 461 - newTranslation); 462 childState.yTranslation = newTranslation; 463 } 464 465 /** 466 * Clamp the height of the child down such that its end is at most on the beginning of 467 * the shelf. 468 * 469 * @param childViewState the view state of the child 470 * @param ambientState the ambient state 471 */ 472 private void clampPositionToShelf(ExpandableViewState childViewState, 473 AmbientState ambientState) { 474 int shelfStart = ambientState.getInnerHeight() 475 - ambientState.getShelf().getIntrinsicHeight(); 476 childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart); 477 if (childViewState.yTranslation >= shelfStart) { 478 childViewState.hidden = true; 479 childViewState.inShelf = true; 480 } 481 if (!ambientState.isShadeExpanded()) { 482 childViewState.height = (int) (mStatusBarHeight - childViewState.yTranslation); 483 } 484 } 485 486 protected int getMaxAllowedChildHeight(View child) { 487 if (child instanceof ExpandableView) { 488 ExpandableView expandableView = (ExpandableView) child; 489 return expandableView.getIntrinsicHeight(); 490 } 491 return child == null? mCollapsedSize : child.getHeight(); 492 } 493 494 /** 495 * Calculate the Z positions for all children based on the number of items in both stacks and 496 * save it in the resultState 497 * @param resultState The result state to update the zTranslation values 498 * @param algorithmState The state in which the current pass of the algorithm is currently in 499 * @param ambientState The ambient state of the algorithm 500 */ 501 private void updateZValuesForState(StackScrollState resultState, 502 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 503 int childCount = algorithmState.visibleChildren.size(); 504 float childrenOnTop = 0.0f; 505 for (int i = childCount - 1; i >= 0; i--) { 506 childrenOnTop = updateChildZValue(i, childrenOnTop, 507 resultState, algorithmState, ambientState); 508 } 509 } 510 511 protected float updateChildZValue(int i, float childrenOnTop, 512 StackScrollState resultState, StackScrollAlgorithmState algorithmState, 513 AmbientState ambientState) { 514 ExpandableView child = algorithmState.visibleChildren.get(i); 515 ExpandableViewState childViewState = resultState.getViewStateForView(child); 516 int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); 517 float baseZ = ambientState.getBaseZHeight(); 518 if (child.mustStayOnScreen() && !ambientState.isDozingAndNotPulsing(child) 519 && childViewState.yTranslation < ambientState.getTopPadding() 520 + ambientState.getStackTranslation()) { 521 if (childrenOnTop != 0.0f) { 522 childrenOnTop++; 523 } else { 524 float overlap = ambientState.getTopPadding() 525 + ambientState.getStackTranslation() - childViewState.yTranslation; 526 childrenOnTop += Math.min(1.0f, overlap / childViewState.height); 527 } 528 childViewState.zTranslation = baseZ 529 + childrenOnTop * zDistanceBetweenElements; 530 } else if (i == 0 && ambientState.isAboveShelf(child)) { 531 // In case this is a new view that has never been measured before, we don't want to 532 // elevate if we are currently expanded more then the notification 533 int shelfHeight = ambientState.getShelf().getIntrinsicHeight(); 534 float shelfStart = ambientState.getInnerHeight() 535 - shelfHeight + ambientState.getTopPadding() 536 + ambientState.getStackTranslation(); 537 float notificationEnd = childViewState.yTranslation + child.getPinnedHeadsUpHeight() 538 + mPaddingBetweenElements; 539 if (shelfStart > notificationEnd) { 540 childViewState.zTranslation = baseZ; 541 } else { 542 float factor = (notificationEnd - shelfStart) / shelfHeight; 543 factor = Math.min(factor, 1.0f); 544 childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements; 545 } 546 } else { 547 childViewState.zTranslation = baseZ; 548 } 549 return childrenOnTop; 550 } 551 552 public void setIsExpanded(boolean isExpanded) { 553 this.mIsExpanded = isExpanded; 554 } 555 556 public class StackScrollAlgorithmState { 557 558 /** 559 * The scroll position of the algorithm 560 */ 561 public int scrollY; 562 563 /** 564 * The children from the host view which are not gone. 565 */ 566 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 567 568 /** 569 * The padding after each child measured in pixels. 570 */ 571 public final HashMap<ExpandableView, Float> paddingMap = new HashMap<>(); 572 573 public int getPaddingAfterChild(ExpandableView child) { 574 Float padding = paddingMap.get(child); 575 if (padding == null) { 576 // Should only happen for the last view 577 return mPaddingBetweenElements; 578 } 579 return (int) padding.floatValue(); 580 } 581 } 582 583} 584