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