blob: 4745f3bd535655357c27ebe521efa25e50f0b1d2 [file] [log] [blame]
/*
* 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;
/**
* 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 static final int MAX_ITEMS_IN_TOP_STACK = 3;
private int mPaddingBetweenElements;
private int mCollapsedSize;
private int mTopStackPeekSize;
private int mBottomStackPeekSize;
private int mZDistanceBetweenElements;
private int mZBasicHeight;
private StackIndentationFunctor mTopStackIndentationFunctor;
private StackIndentationFunctor mBottomStackIndentationFunctor;
private float mLayoutHeight;
private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
public StackScrollAlgorithm(Context context) {
initConstants(context);
}
private void initConstants(Context context) {
// currently the padding is in the elements themself
mPaddingBetweenElements = 0;
mCollapsedSize = context.getResources()
.getDimensionPixelSize(R.dimen.notification_row_min_height);
mTopStackPeekSize = context.getResources()
.getDimensionPixelSize(R.dimen.top_stack_peek_amount);
mBottomStackPeekSize = context.getResources()
.getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
mZDistanceBetweenElements = context.getResources()
.getDimensionPixelSize(R.dimen.z_distance_between_notifications);
mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
MAX_ITEMS_IN_TOP_STACK,
mTopStackPeekSize,
mCollapsedSize + mPaddingBetweenElements,
0.5f);
mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
MAX_ITEMS_IN_BOTTOM_STACK,
mBottomStackPeekSize,
mBottomStackPeekSize,
0.5f);
}
public void getStackScrollState(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();
// The first element is always in there so it's initialized with 1.0f;
algorithmState.itemsInTopStack = 1.0f;
algorithmState.partialInTop = 0.0f;
algorithmState.lastTopStackIndex = 0;
algorithmState.scrollY = resultState.getScrollY();
algorithmState.itemsInBottomStack = 0.0f;
// Phase 1:
findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
// Phase 2:
updatePositionsForState(resultState, algorithmState);
// Phase 3:
updateZValuesForState(resultState, algorithmState);
// write the algorithm state to the result
resultState.setScrollY(algorithmState.scrollY);
}
/**
* 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
* and which will be updated
*/
private void updatePositionsForState(StackScrollState resultState,
StackScrollAlgorithmState algorithmState) {
float stackHeight = getLayoutHeight();
// The position where the bottom stack starts.
float transitioningPositionStart = stackHeight - mCollapsedSize - mBottomStackPeekSize;
// The y coordinate of the current child.
float currentYPosition = 0.0f;
// How far in is the element currently transitioning into the bottom stack.
float yPositionInScrollView = 0.0f;
ViewGroup hostView = resultState.getHostView();
int childCount = hostView.getChildCount();
int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack;
for (int i = 0; i < childCount; i++) {
View child = hostView.getChildAt(i);
StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
childViewState.yTranslation = currentYPosition;
childViewState.location = StackScrollState.ViewState.LOCATION_UNKNOWN;
int childHeight = child.getHeight();
// The y position after this element
float nextYPosition = currentYPosition + childHeight + mPaddingBetweenElements;
float yPositionInScrollViewAfterElement = yPositionInScrollView
+ childHeight
+ mPaddingBetweenElements;
float scrollOffset = yPositionInScrollViewAfterElement - algorithmState.scrollY;
if (i < algorithmState.lastTopStackIndex) {
// Case 1:
// We are in the top Stack
nextYPosition = updateStateForTopStackChild(algorithmState,
numberOfElementsCompletelyIn,
i, childViewState);
} else if (i == algorithmState.lastTopStackIndex) {
// Case 2:
// First element of regular scrollview comes next, so the position is just the
// scrolling position
nextYPosition = Math.min(scrollOffset, transitioningPositionStart);
childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING;
} else if (nextYPosition >= transitioningPositionStart) {
if (currentYPosition >= transitioningPositionStart) {
// Case 3:
// 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
nextYPosition = updateStateForChildFullyInBottomStack(algorithmState,
transitioningPositionStart, childViewState, childHeight);
} else {
// Case 4:
// According to the regular scroll view we are currently translating out of /
// into the bottom of the screen
nextYPosition = updateStateForChildTransitioningInBottom(
algorithmState, stackHeight, transitioningPositionStart,
currentYPosition, childViewState,
childHeight, nextYPosition);
}
} else {
childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
}
// The first card is always rendered.
if (i == 0) {
childViewState.alpha = 1.0f;
childViewState.location = StackScrollState.ViewState.LOCATION_FIRST_CARD;
}
if (childViewState.location == StackScrollState.ViewState.LOCATION_UNKNOWN) {
Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
}
nextYPosition = Math.max(0, nextYPosition);
currentYPosition = nextYPosition;
yPositionInScrollView = yPositionInScrollViewAfterElement;
}
}
private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
float stackHeight, float transitioningPositionStart, float currentYPosition,
StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) {
float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition;
newSize = Math.min(childHeight, newSize);
// Transitioning element on top of bottom stack:
algorithmState.partialInBottom = 1.0f - (
(stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize);
// Our element can be expanded, so we might even have to scroll further than
// mCollapsedSize
algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom);
float offset = mBottomStackIndentationFunctor.getValue(
algorithmState.partialInBottom);
nextYPosition = transitioningPositionStart + offset;
algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
// TODO: only temporarily collapse
if (childHeight != (int) newSize) {
childViewState.height = (int) newSize;
}
childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
return nextYPosition;
}
private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
float transitioningPositionStart, StackScrollState.ViewState childViewState,
int childHeight) {
float nextYPosition;
algorithmState.itemsInBottomStack += 1.0f;
if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
// We are visually entering the bottom stack
nextYPosition = transitioningPositionStart
+ mBottomStackIndentationFunctor.getValue(
algorithmState.itemsInBottomStack);
childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING;
} else {
// we are fully inside the stack
if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
childViewState.alpha = 0.0f;
} else if (algorithmState.itemsInBottomStack
> MAX_ITEMS_IN_BOTTOM_STACK + 1) {
childViewState.alpha = 1.0f - algorithmState.partialInBottom;
}
childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN;
nextYPosition = transitioningPositionStart + mBottomStackPeekSize;
}
// TODO: only temporarily collapse
if (childHeight != mCollapsedSize) {
childViewState.height = mCollapsedSize;
}
return nextYPosition;
}
private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) {
float nextYPosition = 0;
// First we calculate the index relative to the current stack window of size at most
// {@link #MAX_ITEMS_IN_TOP_STACK}
int paddedIndex = i
- Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
if (paddedIndex >= 0) {
// We are currently visually entering the top stack
nextYPosition = mCollapsedSize + mPaddingBetweenElements -
mTopStackIndentationFunctor.getValue(
algorithmState.itemsInTopStack - i - 1);
nextYPosition = Math.min(nextYPosition, mLayoutHeight - mCollapsedSize
- mBottomStackPeekSize);
if (paddedIndex == 0) {
childViewState.alpha = 1.0f - algorithmState.partialInTop;
childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN;
} else {
childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING;
}
} else {
// We are hidden behind the top card and faded out, so we can hide ourselves.
childViewState.alpha = 0.0f;
childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN;
}
return nextYPosition;
}
/**
* Find the number of items in the top stack and update the result state if needed.
*
* @param resultState The result state to update if a height change of an child occurs
* @param algorithmState The state in which the current pass of the algorithm is currently in
* and which will be updated
*/
private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
StackScrollAlgorithmState algorithmState) {
// The y Position if the element would be in a regular scrollView
float yPositionInScrollView = 0.0f;
ViewGroup hostView = resultState.getHostView();
int childCount = hostView.getChildCount();
// find the number of elements in the top stack.
for (int i = 0; i < childCount; i++) {
View child = hostView.getChildAt(i);
StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
int childHeight = child.getHeight();
float yPositionInScrollViewAfterElement = yPositionInScrollView
+ childHeight
+ mPaddingBetweenElements;
if (yPositionInScrollView < algorithmState.scrollY) {
if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) {
// According to the regular scroll view we are fully off screen
algorithmState.itemsInTopStack += 1.0f;
if (childHeight != mCollapsedSize) {
childViewState.height = mCollapsedSize;
}
} else {
// According to the regular scroll view we are partially off screen
// If it is expanded we have to collapse it to a new size
float newSize = yPositionInScrollViewAfterElement
- mPaddingBetweenElements
- algorithmState.scrollY;
// How much did we scroll into this child
algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize
+ mPaddingBetweenElements);
// Our element can be expanded, so this can get negative
algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
algorithmState.itemsInTopStack += algorithmState.partialInTop;
// TODO: handle overlapping sizes with end stack
newSize = Math.max(mCollapsedSize, newSize);
// TODO: only temporarily collapse
if (newSize != childHeight) {
childViewState.height = (int) newSize;
// We decrease scrollY by the same amount we made this child smaller.
// The new scroll position is therefore the start of the element
algorithmState.scrollY = (int) yPositionInScrollView;
resultState.setScrollY(algorithmState.scrollY);
}
if (childHeight > mCollapsedSize) {
// If we are just resizing this child, this element is not treated to be
// transitioning into the stack and therefore it is the last element in
// the stack.
algorithmState.lastTopStackIndex = i;
break;
}
}
} else {
algorithmState.lastTopStackIndex = i;
// We are already past the stack so we can end the loop
break;
}
yPositionInScrollView = yPositionInScrollViewAfterElement;
}
}
/**
* 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
*/
private void updateZValuesForState(StackScrollState resultState,
StackScrollAlgorithmState algorithmState) {
ViewGroup hostView = resultState.getHostView();
int childCount = hostView.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = hostView.getChildAt(i);
StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
if (i < algorithmState.itemsInTopStack) {
float stackIndex = algorithmState.itemsInTopStack - i;
stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2);
childViewState.zTranslation = mZBasicHeight
+ stackIndex * mZDistanceBetweenElements;
} else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
float translationZ = mZBasicHeight
- numItemsAbove * mZDistanceBetweenElements;
childViewState.zTranslation = translationZ;
} else {
childViewState.zTranslation = mZBasicHeight;
}
}
}
public float getLayoutHeight() {
return mLayoutHeight;
}
public void setLayoutHeight(float layoutHeight) {
this.mLayoutHeight = layoutHeight;
}
class StackScrollAlgorithmState {
/**
* The scroll position of the algorithm
*/
public int scrollY;
/**
* The quantity of items which are in the top stack.
*/
public float itemsInTopStack;
/**
* how far in is the element currently transitioning into the top stack
*/
public float partialInTop;
/**
* The last item index which is in the top stack.
* NOTE: In the top stack the item after the transitioning element is also in the stack!
* This is needed to ensure a smooth transition between the y position in the regular
* scrollview and the one in the stack.
*/
public int lastTopStackIndex;
/**
* 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;
}
}