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