StackScrollAlgorithm.java revision 9fe1ee7413d2d914cc24a1123a22dc5b16952b8f
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) { 417 // the first hun can't get off screen. 418 clampHunToMaxTranslation(ambientState, row, childState); 419 } 420 } 421 if (row.isPinned()) { 422 childState.yTranslation = Math.max(childState.yTranslation, 0); 423 childState.height = Math.max(row.getIntrinsicHeight(), childState.height); 424 childState.hidden = false; 425 ExpandableViewState topState = resultState.getViewStateForView(topHeadsUpEntry); 426 if (!isTopEntry && (!mIsExpanded 427 || unmodifiedEndLocation < topState.yTranslation + topState.height)) { 428 // Ensure that a headsUp doesn't vertically extend further than the heads-up at 429 // the top most z-position 430 childState.height = row.getIntrinsicHeight(); 431 childState.yTranslation = topState.yTranslation + topState.height 432 - childState.height; 433 } 434 } 435 if (row.isHeadsUpAnimatingAway()) { 436 childState.hidden = false; 437 } 438 } 439 } 440 441 private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, 442 ExpandableViewState childState) { 443 float newTranslation = Math.max(ambientState.getTopPadding() 444 + ambientState.getStackTranslation(), childState.yTranslation); 445 childState.height = (int) Math.max(childState.height - (newTranslation 446 - childState.yTranslation), row.getCollapsedHeight()); 447 childState.yTranslation = newTranslation; 448 } 449 450 private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, 451 ExpandableViewState childState) { 452 float newTranslation; 453 float bottomPosition = ambientState.getMaxHeadsUpTranslation() - row.getCollapsedHeight(); 454 newTranslation = Math.min(childState.yTranslation, bottomPosition); 455 childState.height = (int) Math.max(childState.height 456 - (childState.yTranslation - newTranslation), row.getCollapsedHeight()); 457 childState.yTranslation = newTranslation; 458 } 459 460 /** 461 * Clamp the height of the child down such that its end is at most on the beginning of 462 * the shelf. 463 * 464 * @param childViewState the view state of the child 465 * @param ambientState the ambient state 466 */ 467 private void clampPositionToShelf(ExpandableViewState childViewState, 468 AmbientState ambientState) { 469 int shelfStart = ambientState.getInnerHeight() 470 - ambientState.getShelf().getIntrinsicHeight(); 471 childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart); 472 if (childViewState.yTranslation >= shelfStart) { 473 childViewState.hidden = true; 474 childViewState.inShelf = true; 475 } 476 if (!ambientState.isShadeExpanded()) { 477 childViewState.height = (int) (mStatusBarHeight - childViewState.yTranslation); 478 } 479 } 480 481 protected int getMaxAllowedChildHeight(View child) { 482 if (child instanceof ExpandableView) { 483 ExpandableView expandableView = (ExpandableView) child; 484 return expandableView.getIntrinsicHeight(); 485 } 486 return child == null? mCollapsedSize : child.getHeight(); 487 } 488 489 /** 490 * Calculate the Z positions for all children based on the number of items in both stacks and 491 * save it in the resultState 492 * @param resultState The result state to update the zTranslation values 493 * @param algorithmState The state in which the current pass of the algorithm is currently in 494 * @param ambientState The ambient state of the algorithm 495 */ 496 private void updateZValuesForState(StackScrollState resultState, 497 StackScrollAlgorithmState algorithmState, AmbientState ambientState) { 498 int childCount = algorithmState.visibleChildren.size(); 499 float childrenOnTop = 0.0f; 500 for (int i = childCount - 1; i >= 0; i--) { 501 childrenOnTop = updateChildZValue(i, childrenOnTop, 502 resultState, algorithmState, ambientState); 503 } 504 } 505 506 protected float updateChildZValue(int i, float childrenOnTop, 507 StackScrollState resultState, StackScrollAlgorithmState algorithmState, 508 AmbientState ambientState) { 509 ExpandableView child = algorithmState.visibleChildren.get(i); 510 ExpandableViewState childViewState = resultState.getViewStateForView(child); 511 int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); 512 float baseZ = ambientState.getBaseZHeight(); 513 if (child.mustStayOnScreen() 514 && childViewState.yTranslation < ambientState.getTopPadding() 515 + ambientState.getStackTranslation()) { 516 if (childrenOnTop != 0.0f) { 517 childrenOnTop++; 518 } else { 519 float overlap = ambientState.getTopPadding() 520 + ambientState.getStackTranslation() - childViewState.yTranslation; 521 childrenOnTop += Math.min(1.0f, overlap / childViewState.height); 522 } 523 childViewState.zTranslation = baseZ 524 + childrenOnTop * zDistanceBetweenElements; 525 } else if (i == 0 && child.isAboveShelf()) { 526 // In case this is a new view that has never been measured before, we don't want to 527 // elevate if we are currently expanded more then the notification 528 int shelfHeight = ambientState.getShelf().getIntrinsicHeight(); 529 float shelfStart = ambientState.getInnerHeight() 530 - shelfHeight + ambientState.getTopPadding() 531 + ambientState.getStackTranslation(); 532 float notificationEnd = childViewState.yTranslation + child.getPinnedHeadsUpHeight() 533 + mPaddingBetweenElements; 534 if (shelfStart > notificationEnd) { 535 childViewState.zTranslation = baseZ; 536 } else { 537 float factor = (notificationEnd - shelfStart) / shelfHeight; 538 factor = Math.min(factor, 1.0f); 539 childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements; 540 } 541 } else { 542 childViewState.zTranslation = baseZ; 543 } 544 return childrenOnTop; 545 } 546 547 public void setIsExpanded(boolean isExpanded) { 548 this.mIsExpanded = isExpanded; 549 } 550 551 public class StackScrollAlgorithmState { 552 553 /** 554 * The scroll position of the algorithm 555 */ 556 public int scrollY; 557 558 /** 559 * The children from the host view which are not gone. 560 */ 561 public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); 562 563 /** 564 * The padding after each child measured in pixels. 565 */ 566 public final HashMap<ExpandableView, Float> paddingMap = new HashMap<>(); 567 568 public int getPaddingAfterChild(ExpandableView child) { 569 Float padding = paddingMap.get(child); 570 if (padding == null) { 571 // Should only happen for the last view 572 return mPaddingBetweenElements; 573 } 574 return (int) padding.floatValue(); 575 } 576 } 577 578} 579