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