StackScrollAlgorithm.java revision 67b2260093774f5866f781aede52830440f4ed0e
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.view.View;
21import android.view.ViewGroup;
22import com.android.systemui.R;
23
24/**
25 * The Algorithm of the {@link com.android.systemui.statusbar.stack
26 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
27 * .stack.StackScrollState}
28 */
29public class StackScrollAlgorithm {
30
31    private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
32    private static final int MAX_ITEMS_IN_TOP_STACK = 3;
33
34    private int mPaddingBetweenElements;
35    private int mCollapsedSize;
36    private int mTopStackPeekSize;
37    private int mBottomStackPeekSize;
38    private int mZDistanceBetweenElements;
39    private int mZBasicHeight;
40
41    private StackIndentationFunctor mTopStackIndentationFunctor;
42    private StackIndentationFunctor mBottomStackIndentationFunctor;
43
44    private float mLayoutHeight;
45    private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
46
47    public StackScrollAlgorithm(Context context) {
48        initConstants(context);
49    }
50
51    private void initConstants(Context context) {
52
53        // currently the padding is in the elements themself
54        mPaddingBetweenElements = 0;
55        mCollapsedSize = context.getResources()
56                .getDimensionPixelSize(R.dimen.notification_row_min_height);
57        mTopStackPeekSize = context.getResources()
58                .getDimensionPixelSize(R.dimen.top_stack_peek_amount);
59        mBottomStackPeekSize = context.getResources()
60                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
61        mZDistanceBetweenElements = context.getResources()
62                .getDimensionPixelSize(R.dimen.z_distance_between_notifications);
63        mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
64
65        mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
66                MAX_ITEMS_IN_TOP_STACK,
67                mTopStackPeekSize,
68                mCollapsedSize + mPaddingBetweenElements,
69                0.5f);
70        mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
71                MAX_ITEMS_IN_BOTTOM_STACK,
72                mBottomStackPeekSize,
73                mBottomStackPeekSize,
74                0.5f);
75    }
76
77
78    public void getStackScrollState(StackScrollState resultState) {
79        // The state of the local variables are saved in an algorithmState to easily subdivide it
80        // into multiple phases.
81        StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
82
83        // First we reset the view states to their default values.
84        resultState.resetViewStates();
85
86        // The first element is always in there so it's initialized with 1.0f.
87        algorithmState.itemsInTopStack = 1.0f;
88        algorithmState.partialInTop = 0.0f;
89        algorithmState.lastTopStackIndex = 0;
90        algorithmState.scrollY = resultState.getScrollY();
91        algorithmState.itemsInBottomStack = 0.0f;
92
93        // Phase 1:
94        findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
95
96        // Phase 2:
97        updatePositionsForState(resultState, algorithmState);
98
99        // Phase 3:
100        updateZValuesForState(resultState, algorithmState);
101
102        // Write the algorithm state to the result.
103        resultState.setScrollY(algorithmState.scrollY);
104    }
105
106    /**
107     * Determine the positions for the views. This is the main part of the algorithm.
108     *
109     * @param resultState The result state to update if a change to the properties of a child occurs
110     * @param algorithmState The state in which the current pass of the algorithm is currently in
111     *                       and which will be updated
112     */
113    private void updatePositionsForState(StackScrollState resultState,
114            StackScrollAlgorithmState algorithmState) {
115        float stackHeight = getLayoutHeight();
116
117        // The position where the bottom stack starts.
118        float transitioningPositionStart = stackHeight - mCollapsedSize - mBottomStackPeekSize;
119
120        // The y coordinate of the current child.
121        float currentYPosition = 0.0f;
122
123        // How far in is the element currently transitioning into the bottom stack.
124        float yPositionInScrollView = 0.0f;
125
126        ViewGroup hostView = resultState.getHostView();
127        int childCount = hostView.getChildCount();
128        int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack;
129        for (int i = 0; i < childCount; i++) {
130            View child = hostView.getChildAt(i);
131            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
132            childViewState.yTranslation = currentYPosition;
133            int childHeight = child.getHeight();
134            // The y position after this element
135            float nextYPosition = currentYPosition + childHeight + mPaddingBetweenElements;
136            float yPositionInScrollViewAfterElement = yPositionInScrollView
137                    + childHeight
138                    + mPaddingBetweenElements;
139            float scrollOffset = yPositionInScrollViewAfterElement - algorithmState.scrollY;
140            if (i < algorithmState.lastTopStackIndex) {
141                // Case 1:
142                // We are in the top Stack
143                nextYPosition = updateStateForTopStackChild(algorithmState,
144                        numberOfElementsCompletelyIn,
145                        i, childViewState);
146
147            } else if (i == algorithmState.lastTopStackIndex) {
148                // Case 2:
149                // First element of regular scrollview comes next, so the position is just the
150                // scrolling position
151                nextYPosition = scrollOffset;
152            } else if (nextYPosition >= transitioningPositionStart) {
153                if (currentYPosition >= transitioningPositionStart) {
154                    // Case 3:
155                    // According to the regular scroll view we are fully translated out of the
156                    // bottom of the screen so we are fully in the bottom stack
157                    nextYPosition = updateStateForChildFullyInBottomStack(algorithmState,
158                            transitioningPositionStart, childViewState, childHeight);
159
160
161                } else {
162                    // Case 4:
163                    // According to the regular scroll view we are currently translating out of /
164                    // into the bottom of the screen
165                    nextYPosition = updateStateForChildTransitioningInBottom(
166                            algorithmState, stackHeight, transitioningPositionStart,
167                            currentYPosition, childViewState,
168                            childHeight, nextYPosition);
169                }
170            }
171            currentYPosition = nextYPosition;
172            yPositionInScrollView = yPositionInScrollViewAfterElement;
173        }
174    }
175
176    private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
177            float stackHeight, float transitioningPositionStart, float currentYPosition,
178            StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) {
179        float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition;
180        newSize = Math.min(childHeight, newSize);
181        // Transitioning element on top of bottom stack:
182        algorithmState.partialInBottom = 1.0f - (
183                (stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize);
184        // Our element can be expanded, so we might even have to scroll further than
185        // mCollapsedSize
186        algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom);
187        float offset = mBottomStackIndentationFunctor.getValue(
188                algorithmState.partialInBottom);
189        nextYPosition = transitioningPositionStart + offset;
190        algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
191        // TODO: only temporarily collapse
192        if (childHeight != (int) newSize) {
193            childViewState.height = (int) newSize;
194        }
195        return nextYPosition;
196    }
197
198    private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
199            float transitioningPositionStart, StackScrollState.ViewState childViewState,
200            int childHeight) {
201
202        float nextYPosition;
203        algorithmState.itemsInBottomStack += 1.0f;
204        if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
205            // We are visually entering the bottom stack
206            nextYPosition = transitioningPositionStart
207                    + mBottomStackIndentationFunctor.getValue(
208                            algorithmState.itemsInBottomStack);
209        } else {
210            // we are fully inside the stack
211            if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
212                childViewState.alpha = 0.0f;
213            } else if (algorithmState.itemsInBottomStack
214                    > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
215                childViewState.alpha = 1.0f - algorithmState.partialInBottom;
216            }
217            nextYPosition = transitioningPositionStart + mBottomStackPeekSize;
218        }
219        // TODO: only temporarily collapse
220        if (childHeight != mCollapsedSize) {
221            childViewState.height = mCollapsedSize;
222        }
223        return nextYPosition;
224    }
225
226    private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
227            int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) {
228
229        float nextYPosition = 0;
230
231        // First we calculate the index relative to the current stack window of size at most
232        // {@link #MAX_ITEMS_IN_TOP_STACK}
233        int paddedIndex = i
234                - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
235        if (paddedIndex >= 0) {
236            // We are currently visually entering the top stack
237            nextYPosition = mCollapsedSize + mPaddingBetweenElements -
238                    mTopStackIndentationFunctor.getValue(
239                            algorithmState.itemsInTopStack - i - 1);
240            if (paddedIndex == 0 && i != 0) {
241                childViewState.alpha = 1.0f - algorithmState.partialInTop;
242            }
243        } else {
244            // We are hidden behind the top card and faded out, so we can hide ourselfs
245            if (i != 0) {
246                childViewState.alpha = 0.0f;
247            }
248        }
249        return nextYPosition;
250    }
251
252    /**
253     * Find the number of items in the top stack and update the result state if needed.
254     *
255     * @param resultState The result state to update if a height change of an child occurs
256     * @param algorithmState The state in which the current pass of the algorithm is currently in
257     *                       and which will be updated
258     */
259    private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
260            StackScrollAlgorithmState algorithmState) {
261
262        // The y Position if the element would be in a regular scrollView
263        float yPositionInScrollView = 0.0f;
264        ViewGroup hostView = resultState.getHostView();
265        int childCount = hostView.getChildCount();
266
267        // find the number of elements in the top stack.
268        for (int i = 0; i < childCount; i++) {
269            View child = hostView.getChildAt(i);
270            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
271            int childHeight = child.getHeight();
272            float yPositionInScrollViewAfterElement = yPositionInScrollView
273                    + childHeight
274                    + mPaddingBetweenElements;
275            if (yPositionInScrollView < algorithmState.scrollY) {
276                if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) {
277                    // According to the regular scroll view we are fully off screen
278                    algorithmState.itemsInTopStack += 1.0f;
279                    if (childHeight != mCollapsedSize) {
280                        childViewState.height = mCollapsedSize;
281                    }
282                } else {
283                    // According to the regular scroll view we are partially off screen
284                    // If it is expanded we have to collapse it to a new size
285                    float newSize = yPositionInScrollViewAfterElement
286                            - mPaddingBetweenElements
287                            - algorithmState.scrollY;
288
289                    // How much did we scroll into this child
290                    algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize
291                            + mPaddingBetweenElements);
292
293                    // Our element can be expanded, so this can get negative
294                    algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
295                    algorithmState.itemsInTopStack += algorithmState.partialInTop;
296                    // TODO: handle overlapping sizes with end stack
297                    newSize = Math.max(mCollapsedSize, newSize);
298                    // TODO: only temporarily collapse
299                    if (newSize != childHeight) {
300                        childViewState.height = (int) newSize;
301
302                        // We decrease scrollY by the same amount we made this child smaller.
303                        // The new scroll position is therefore the start of the element
304                        algorithmState.scrollY = (int) yPositionInScrollView;
305                        resultState.setScrollY(algorithmState.scrollY);
306                    }
307                    if (childHeight > mCollapsedSize) {
308                        // If we are just resizing this child, this element is not treated to be
309                        // transitioning into the stack and therefore it is the last element in
310                        // the stack.
311                        algorithmState.lastTopStackIndex = i;
312                        break;
313                    }
314                }
315            } else {
316                algorithmState.lastTopStackIndex = i;
317
318                // We are already past the stack so we can end the loop
319                break;
320            }
321            yPositionInScrollView = yPositionInScrollViewAfterElement;
322        }
323    }
324
325    /**
326     * Calculate the Z positions for all children based on the number of items in both stacks and
327     * save it in the resultState
328     *
329     * @param resultState The result state to update the zTranslation values
330     * @param algorithmState The state in which the current pass of the algorithm is currently in
331     */
332    private void updateZValuesForState(StackScrollState resultState,
333            StackScrollAlgorithmState algorithmState) {
334        ViewGroup hostView = resultState.getHostView();
335        int childCount = hostView.getChildCount();
336        for (int i = 0; i < childCount; i++) {
337            View child = hostView.getChildAt(i);
338            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
339            if (i < algorithmState.itemsInTopStack) {
340                float stackIndex = algorithmState.itemsInTopStack - i;
341                stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2);
342                childViewState.zTranslation = mZBasicHeight
343                        + stackIndex * mZDistanceBetweenElements;
344            } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
345                float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
346                float translationZ = mZBasicHeight
347                        - numItemsAbove * mZDistanceBetweenElements;
348                childViewState.zTranslation = translationZ;
349            } else {
350                childViewState.zTranslation = mZBasicHeight;
351            }
352        }
353    }
354
355    public float getLayoutHeight() {
356        return mLayoutHeight;
357    }
358
359    public void setLayoutHeight(float layoutHeight) {
360        this.mLayoutHeight = layoutHeight;
361    }
362
363    class StackScrollAlgorithmState {
364
365        /**
366         * The scroll position of the algorithm
367         */
368        public int scrollY;
369
370        /**
371         *  The quantity of items which are in the top stack.
372         */
373        public float itemsInTopStack;
374
375        /**
376         * how far in is the element currently transitioning into the top stack
377         */
378        public float partialInTop;
379
380        /**
381         * The last item index which is in the top stack.
382         * NOTE: In the top stack the item after the transitioning element is also in the stack!
383         * This is needed to ensure a smooth transition between the y position in the regular
384         * scrollview and the one in the stack.
385         */
386        public int lastTopStackIndex;
387
388        /**
389         * The quantity of items which are in the bottom stack.
390         */
391        public float itemsInBottomStack;
392
393        /**
394         * how far in is the element currently transitioning into the bottom stack
395         */
396        public float partialInBottom;
397    }
398
399}
400