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