/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar.stack; import android.content.Context; import android.util.Log; import android.view.View; import android.view.ViewGroup; import com.android.systemui.R; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.ExpandableView; import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.NotificationUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * The Algorithm of the {@link com.android.systemui.statusbar.stack * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar * .stack.StackScrollState} */ public class StackScrollAlgorithm { private static final String LOG_TAG = "StackScrollAlgorithm"; private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3; private int mPaddingBetweenElements; private int mIncreasedPaddingBetweenElements; private int mCollapsedSize; private int mBottomStackPeekSize; private int mZDistanceBetweenElements; private int mZBasicHeight; private StackIndentationFunctor mBottomStackIndentationFunctor; private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState(); private boolean mIsExpanded; private int mBottomStackSlowDownLength; public StackScrollAlgorithm(Context context) { initView(context); } public void initView(Context context) { initConstants(context); } public int getBottomStackSlowDownLength() { return mBottomStackSlowDownLength + mPaddingBetweenElements; } private void initConstants(Context context) { mPaddingBetweenElements = Math.max(1, context.getResources() .getDimensionPixelSize(R.dimen.notification_divider_height)); mIncreasedPaddingBetweenElements = context.getResources() .getDimensionPixelSize(R.dimen.notification_divider_height_increased); mCollapsedSize = context.getResources() .getDimensionPixelSize(R.dimen.notification_min_height); mBottomStackPeekSize = context.getResources() .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount); mZDistanceBetweenElements = Math.max(1, context.getResources() .getDimensionPixelSize(R.dimen.z_distance_between_notifications)); mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements; mBottomStackSlowDownLength = context.getResources() .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length); mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor( MAX_ITEMS_IN_BOTTOM_STACK, mBottomStackPeekSize, getBottomStackSlowDownLength(), 0.5f); } public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) { // The state of the local variables are saved in an algorithmState to easily subdivide it // into multiple phases. StackScrollAlgorithmState algorithmState = mTempAlgorithmState; // First we reset the view states to their default values. resultState.resetViewStates(); initAlgorithmState(resultState, algorithmState, ambientState); updatePositionsForState(resultState, algorithmState, ambientState); updateZValuesForState(resultState, algorithmState, ambientState); updateHeadsUpStates(resultState, algorithmState, ambientState); handleDraggedViews(ambientState, resultState, algorithmState); updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState); updateClipping(resultState, algorithmState, ambientState); updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex()); getNotificationChildrenStates(resultState, algorithmState); } private void getNotificationChildrenStates(StackScrollState resultState, StackScrollAlgorithmState algorithmState) { int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableView v = algorithmState.visibleChildren.get(i); if (v instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) v; row.getChildrenStates(resultState); } } } private void updateSpeedBumpState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, int speedBumpIndex) { int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); StackViewState childViewState = resultState.getViewStateForView(child); // The speed bump can also be gone, so equality needs to be taken when comparing // indices. childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex; } } private void updateClipping(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState) { float drawStart = ambientState.getTopPadding() + ambientState.getStackTranslation(); float previousNotificationEnd = 0; float previousNotificationStart = 0; int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableView child = algorithmState.visibleChildren.get(i); StackViewState state = resultState.getViewStateForView(child); if (!child.mustStayOnScreen()) { previousNotificationEnd = Math.max(drawStart, previousNotificationEnd); previousNotificationStart = Math.max(drawStart, previousNotificationStart); } float newYTranslation = state.yTranslation; float newHeight = state.height; float newNotificationEnd = newYTranslation + newHeight; boolean isHeadsUp = (child instanceof ExpandableNotificationRow) && ((ExpandableNotificationRow) child).isPinned(); if (newYTranslation < previousNotificationEnd && (!isHeadsUp || ambientState.isShadeExpanded())) { // The previous view is overlapping on top, clip! float overlapAmount = previousNotificationEnd - newYTranslation; state.clipTopAmount = (int) overlapAmount; } else { state.clipTopAmount = 0; } if (!child.isTransparent()) { // Only update the previous values if we are not transparent, // otherwise we would clip to a transparent view. previousNotificationEnd = newNotificationEnd; previousNotificationStart = newYTranslation; } } } public static boolean canChildBeDismissed(View v) { if (!(v instanceof ExpandableNotificationRow)) { return false; } ExpandableNotificationRow row = (ExpandableNotificationRow) v; if (row.areGutsExposed()) { return false; } return row.canViewBeDismissed(); } /** * Updates the dimmed, activated and hiding sensitive states of the children. */ private void updateDimmedActivatedHideSensitive(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState) { boolean dimmed = ambientState.isDimmed(); boolean dark = ambientState.isDark(); boolean hideSensitive = ambientState.isHideSensitive(); View activatedChild = ambientState.getActivatedChild(); int childCount = algorithmState.visibleChildren.size(); for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); StackViewState childViewState = resultState.getViewStateForView(child); childViewState.dimmed = dimmed; childViewState.dark = dark; childViewState.hideSensitive = hideSensitive; boolean isActivatedChild = activatedChild == child; if (dimmed && isActivatedChild) { childViewState.zTranslation += 2.0f * mZDistanceBetweenElements; } } } /** * Handle the special state when views are being dragged */ private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState, StackScrollAlgorithmState algorithmState) { ArrayList<View> draggedViews = ambientState.getDraggedViews(); for (View draggedView : draggedViews) { int childIndex = algorithmState.visibleChildren.indexOf(draggedView); if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) { View nextChild = algorithmState.visibleChildren.get(childIndex + 1); if (!draggedViews.contains(nextChild)) { // only if the view is not dragged itself we modify its state to be fully // visible StackViewState viewState = resultState.getViewStateForView( nextChild); // The child below the dragged one must be fully visible if (ambientState.isShadeExpanded()) { viewState.shadowAlpha = 1; viewState.hidden = false; } } // Lets set the alpha to the one it currently has, as its currently being dragged StackViewState viewState = resultState.getViewStateForView(draggedView); // The dragged child should keep the set alpha viewState.alpha = draggedView.getAlpha(); } } } /** * Initialize the algorithm state like updating the visible children. */ private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state, AmbientState ambientState) { state.itemsInBottomStack = 0.0f; state.partialInBottom = 0.0f; float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */); int scrollY = ambientState.getScrollY(); // Due to the overScroller, the stackscroller can have negative scroll state. This is // already accounted for by the top padding and doesn't need an additional adaption scrollY = Math.max(0, scrollY); state.scrollY = (int) (scrollY + bottomOverScroll); //now init the visible children and update paddings ViewGroup hostView = resultState.getHostView(); int childCount = hostView.getChildCount(); state.visibleChildren.clear(); state.visibleChildren.ensureCapacity(childCount); state.increasedPaddingMap.clear(); int notGoneIndex = 0; ExpandableView lastView = null; for (int i = 0; i < childCount; i++) { ExpandableView v = (ExpandableView) hostView.getChildAt(i); if (v.getVisibility() != View.GONE) { notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v); float increasedPadding = v.getIncreasedPaddingAmount(); if (increasedPadding != 0.0f) { state.increasedPaddingMap.put(v, increasedPadding); if (lastView != null) { Float prevValue = state.increasedPaddingMap.get(lastView); float newValue = prevValue != null ? Math.max(prevValue, increasedPadding) : increasedPadding; state.increasedPaddingMap.put(lastView, newValue); } } if (v instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) v; // handle the notgoneIndex for the children as well List<ExpandableNotificationRow> children = row.getNotificationChildren(); if (row.isSummaryWithChildren() && children != null) { for (ExpandableNotificationRow childRow : children) { if (childRow.getVisibility() != View.GONE) { StackViewState childState = resultState.getViewStateForView(childRow); childState.notGoneIndex = notGoneIndex; notGoneIndex++; } } } } lastView = v; } } } private int updateNotGoneIndex(StackScrollState resultState, StackScrollAlgorithmState state, int notGoneIndex, ExpandableView v) { StackViewState viewState = resultState.getViewStateForView(v); viewState.notGoneIndex = notGoneIndex; state.visibleChildren.add(v); notGoneIndex++; return notGoneIndex; } /** * Determine the positions for the views. This is the main part of the algorithm. * * @param resultState The result state to update if a change to the properties of a child occurs * @param algorithmState The state in which the current pass of the algorithm is currently in * @param ambientState The current ambient state */ private void updatePositionsForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState) { // The starting position of the bottom stack peek float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize; // The position where the bottom stack starts. float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength; // The y coordinate of the current child. float currentYPosition = -algorithmState.scrollY; int childCount = algorithmState.visibleChildren.size(); int paddingAfterChild; for (int i = 0; i < childCount; i++) { ExpandableView child = algorithmState.visibleChildren.get(i); StackViewState childViewState = resultState.getViewStateForView(child); childViewState.location = StackViewState.LOCATION_UNKNOWN; paddingAfterChild = getPaddingAfterChild(algorithmState, child); int childHeight = getMaxAllowedChildHeight(child); int collapsedHeight = child.getCollapsedHeight(); childViewState.yTranslation = currentYPosition; if (i == 0) { updateFirstChildHeight(child, childViewState, childHeight, ambientState); } // The y position after this element float nextYPosition = currentYPosition + childHeight + paddingAfterChild; if (nextYPosition >= bottomStackStart) { // Case 1: // We are in the bottom stack. if (currentYPosition >= bottomStackStart) { // According to the regular scroll view we are fully translated out of the // bottom of the screen so we are fully in the bottom stack updateStateForChildFullyInBottomStack(algorithmState, bottomStackStart, childViewState, collapsedHeight, ambientState, child); } else { // According to the regular scroll view we are currently translating out of / // into the bottom of the screen updateStateForChildTransitioningInBottom(algorithmState, bottomStackStart, child, currentYPosition, childViewState, childHeight); } } else { // Case 2: // We are in the regular scroll area. childViewState.location = StackViewState.LOCATION_MAIN_AREA; clampPositionToBottomStackStart(childViewState, childViewState.height, childHeight, ambientState); } if (i == 0 && ambientState.getScrollY() <= 0) { // The first card can get into the bottom stack if it's the only one // on the lockscreen which pushes it up. Let's make sure that doesn't happen and // it stays at the top childViewState.yTranslation = Math.max(0, childViewState.yTranslation); } currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild; if (currentYPosition <= 0) { childViewState.location = StackViewState.LOCATION_HIDDEN_TOP; } if (childViewState.location == StackViewState.LOCATION_UNKNOWN) { Log.wtf(LOG_TAG, "Failed to assign location for child " + i); } childViewState.yTranslation += ambientState.getTopPadding() + ambientState.getStackTranslation(); } } private int getPaddingAfterChild(StackScrollAlgorithmState algorithmState, ExpandableView child) { Float paddingValue = algorithmState.increasedPaddingMap.get(child); return paddingValue == null ? mPaddingBetweenElements : (int) NotificationUtils.interpolate(mPaddingBetweenElements, mIncreasedPaddingBetweenElements, paddingValue); } private void updateHeadsUpStates(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); ExpandableNotificationRow topHeadsUpEntry = null; for (int i = 0; i < childCount; i++) { View child = algorithmState.visibleChildren.get(i); if (!(child instanceof ExpandableNotificationRow)) { break; } ExpandableNotificationRow row = (ExpandableNotificationRow) child; if (!row.isHeadsUp()) { break; } StackViewState childState = resultState.getViewStateForView(row); if (topHeadsUpEntry == null) { topHeadsUpEntry = row; childState.location = StackViewState.LOCATION_FIRST_HUN; } boolean isTopEntry = topHeadsUpEntry == row; float unmodifiedEndLocation = childState.yTranslation + childState.height; if (mIsExpanded) { // Ensure that the heads up is always visible even when scrolled off clampHunToTop(ambientState, row, childState); clampHunToMaxTranslation(ambientState, row, childState); } if (row.isPinned()) { childState.yTranslation = Math.max(childState.yTranslation, 0); childState.height = Math.max(row.getIntrinsicHeight(), childState.height); StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry); if (!isTopEntry && (!mIsExpanded || unmodifiedEndLocation < topState.yTranslation + topState.height)) { // Ensure that a headsUp doesn't vertically extend further than the heads-up at // the top most z-position childState.height = row.getIntrinsicHeight(); childState.yTranslation = topState.yTranslation + topState.height - childState.height; } } } } private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row, StackViewState childState) { float newTranslation = Math.max(ambientState.getTopPadding() + ambientState.getStackTranslation(), childState.yTranslation); childState.height = (int) Math.max(childState.height - (newTranslation - childState.yTranslation), row.getCollapsedHeight()); childState.yTranslation = newTranslation; } private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row, StackViewState childState) { float newTranslation; float bottomPosition = ambientState.getMaxHeadsUpTranslation() - row.getCollapsedHeight(); newTranslation = Math.min(childState.yTranslation, bottomPosition); childState.height = (int) Math.max(childState.height - (childState.yTranslation - newTranslation), row.getCollapsedHeight()); childState.yTranslation = newTranslation; } /** * Clamp the yTranslation of the child down such that its end is at most on the beginning of * the bottom stack. * * @param childViewState the view state of the child * @param childHeight the height of this child * @param minHeight the minumum Height of the View */ private void clampPositionToBottomStackStart(StackViewState childViewState, int childHeight, int minHeight, AmbientState ambientState) { int bottomStackStart = ambientState.getInnerHeight() - mBottomStackPeekSize - mBottomStackSlowDownLength; int childStart = bottomStackStart - childHeight; if (childStart < childViewState.yTranslation) { float newHeight = bottomStackStart - childViewState.yTranslation; if (newHeight < minHeight) { newHeight = minHeight; childViewState.yTranslation = bottomStackStart - minHeight; } childViewState.height = (int) newHeight; } } private int getMaxAllowedChildHeight(View child) { if (child instanceof ExpandableView) { ExpandableView expandableView = (ExpandableView) child; return expandableView.getIntrinsicHeight(); } return child == null? mCollapsedSize : child.getHeight(); } private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, ExpandableView child, float currentYPosition, StackViewState childViewState, int childHeight) { // This is the transitioning element on top of bottom stack, calculate how far we are in. algorithmState.partialInBottom = 1.0f - ( (transitioningPositionStart - currentYPosition) / (childHeight + getPaddingAfterChild(algorithmState, child))); // the offset starting at the transitionPosition of the bottom stack float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom); algorithmState.itemsInBottomStack += algorithmState.partialInBottom; int newHeight = childHeight; if (childHeight > child.getCollapsedHeight()) { newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset - getPaddingAfterChild(algorithmState, child) - currentYPosition, childHeight), child.getCollapsedHeight()); childViewState.height = newHeight; } childViewState.yTranslation = transitioningPositionStart + offset - newHeight - getPaddingAfterChild(algorithmState, child); childViewState.location = StackViewState.LOCATION_MAIN_AREA; } private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState, float transitioningPositionStart, StackViewState childViewState, int collapsedHeight, AmbientState ambientState, ExpandableView child) { float currentYPosition; algorithmState.itemsInBottomStack += 1.0f; if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) { // We are visually entering the bottom stack currentYPosition = transitioningPositionStart + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack) - getPaddingAfterChild(algorithmState, child); childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING; } else { // we are fully inside the stack if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) { childViewState.hidden = true; childViewState.shadowAlpha = 0.0f; } else if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 1) { childViewState.shadowAlpha = 1.0f - algorithmState.partialInBottom; } childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN; currentYPosition = ambientState.getInnerHeight(); } childViewState.height = collapsedHeight; childViewState.yTranslation = currentYPosition - collapsedHeight; } /** * Update the height of the first child i.e clamp it to the bottom stack * * @param child the child to update * @param childViewState the viewstate of the child * @param childHeight the height of the child * @param ambientState The ambient state of the algorithm */ private void updateFirstChildHeight(ExpandableView child, StackViewState childViewState, int childHeight, AmbientState ambientState) { // The starting position of the bottom stack peek int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize - mBottomStackSlowDownLength + ambientState.getScrollY(); // Collapse and expand the first child while the shade is being expanded childViewState.height = (int) Math.max(Math.min(bottomPeekStart, (float) childHeight), child.getCollapsedHeight()); } /** * Calculate the Z positions for all children based on the number of items in both stacks and * save it in the resultState * @param resultState The result state to update the zTranslation values * @param algorithmState The state in which the current pass of the algorithm is currently in * @param ambientState The ambient state of the algorithm */ private void updateZValuesForState(StackScrollState resultState, StackScrollAlgorithmState algorithmState, AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); float childrenOnTop = 0.0f; for (int i = childCount - 1; i >= 0; i--) { ExpandableView child = algorithmState.visibleChildren.get(i); StackViewState childViewState = resultState.getViewStateForView(child); if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) { // We are in the bottom stack float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack); float zSubtraction; if (numItemsAbove <= 1.0f) { float factor = 0.2f; // Lets fade in slower to the threshold to make the shadow fade in look nicer if (numItemsAbove <= factor) { zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD * numItemsAbove * (1.0f / factor); } else { zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD + (numItemsAbove - factor) * (1.0f / (1.0f - factor)) * (mZDistanceBetweenElements - FakeShadowView.SHADOW_SIBLING_TRESHOLD); } } else { zSubtraction = numItemsAbove * mZDistanceBetweenElements; } childViewState.zTranslation = mZBasicHeight - zSubtraction; } else if (child.mustStayOnScreen() && childViewState.yTranslation < ambientState.getTopPadding() + ambientState.getStackTranslation()) { if (childrenOnTop != 0.0f) { childrenOnTop++; } else { float overlap = ambientState.getTopPadding() + ambientState.getStackTranslation() - childViewState.yTranslation; childrenOnTop += Math.min(1.0f, overlap / childViewState.height); } childViewState.zTranslation = mZBasicHeight + childrenOnTop * mZDistanceBetweenElements; } else { childViewState.zTranslation = mZBasicHeight; } } } private boolean isMaxSizeInitialized(ExpandableView child) { if (child instanceof ExpandableNotificationRow) { ExpandableNotificationRow row = (ExpandableNotificationRow) child; return row.isMaxExpandHeightInitialized(); } return child == null || child.getWidth() != 0; } private View findFirstVisibleChild(ViewGroup container) { int childCount = container.getChildCount(); for (int i = 0; i < childCount; i++) { View child = container.getChildAt(i); if (child.getVisibility() != View.GONE) { return child; } } return null; } public void setIsExpanded(boolean isExpanded) { this.mIsExpanded = isExpanded; } class StackScrollAlgorithmState { /** * The scroll position of the algorithm */ public int scrollY; /** * The quantity of items which are in the bottom stack. */ public float itemsInBottomStack; /** * how far in is the element currently transitioning into the bottom stack */ public float partialInBottom; /** * The children from the host view which are not gone. */ public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>(); /** * The children from the host that need an increased padding after them. A value of 0 means * no increased padding, a value of 1 means full padding. */ public final HashMap<ExpandableView, Float> increasedPaddingMap = new HashMap<>(); } }