StackScrollAlgorithm.java revision 6e3ecebcec1b82fd81f6d78b8deb5c4189b6026e
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 = scrollOffset;
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            currentYPosition = nextYPosition;
184            yPositionInScrollView = yPositionInScrollViewAfterElement;
185        }
186    }
187
188    private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
189            float stackHeight, float transitioningPositionStart, float currentYPosition,
190            StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) {
191        float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition;
192        newSize = Math.min(childHeight, newSize);
193        // Transitioning element on top of bottom stack:
194        algorithmState.partialInBottom = 1.0f - (
195                (stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize);
196        // Our element can be expanded, so we might even have to scroll further than
197        // mCollapsedSize
198        algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom);
199        float offset = mBottomStackIndentationFunctor.getValue(
200                algorithmState.partialInBottom);
201        nextYPosition = transitioningPositionStart + offset;
202        algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
203        // TODO: only temporarily collapse
204        if (childHeight != (int) newSize) {
205            childViewState.height = (int) newSize;
206        }
207        childViewState.location = StackScrollState.ViewState.LOCATION_MAIN_AREA;
208
209        return nextYPosition;
210    }
211
212    private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
213            float transitioningPositionStart, StackScrollState.ViewState childViewState,
214            int childHeight) {
215
216        float nextYPosition;
217        algorithmState.itemsInBottomStack += 1.0f;
218        if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
219            // We are visually entering the bottom stack
220            nextYPosition = transitioningPositionStart
221                    + mBottomStackIndentationFunctor.getValue(
222                            algorithmState.itemsInBottomStack);
223            childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_PEEKING;
224        } else {
225            // we are fully inside the stack
226            if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
227                childViewState.alpha = 0.0f;
228            } else if (algorithmState.itemsInBottomStack
229                    > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
230                childViewState.alpha = 1.0f - algorithmState.partialInBottom;
231            }
232            childViewState.location = StackScrollState.ViewState.LOCATION_BOTTOM_STACK_HIDDEN;
233            nextYPosition = transitioningPositionStart + mBottomStackPeekSize;
234        }
235        // TODO: only temporarily collapse
236        if (childHeight != mCollapsedSize) {
237            childViewState.height = mCollapsedSize;
238        }
239        return nextYPosition;
240    }
241
242    private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
243            int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) {
244
245        float nextYPosition = 0;
246
247        // First we calculate the index relative to the current stack window of size at most
248        // {@link #MAX_ITEMS_IN_TOP_STACK}
249        int paddedIndex = i
250                - Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
251        if (paddedIndex >= 0) {
252            // We are currently visually entering the top stack
253            nextYPosition = mCollapsedSize + mPaddingBetweenElements -
254                    mTopStackIndentationFunctor.getValue(
255                            algorithmState.itemsInTopStack - i - 1);
256            if (paddedIndex == 0) {
257                childViewState.alpha = 1.0f - algorithmState.partialInTop;
258                childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN;
259            } else {
260                childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_PEEKING;
261            }
262        } else {
263            // We are hidden behind the top card and faded out, so we can hide ourselves.
264            childViewState.alpha = 0.0f;
265            childViewState.location = StackScrollState.ViewState.LOCATION_TOP_STACK_HIDDEN;
266        }
267        return nextYPosition;
268    }
269
270    /**
271     * Find the number of items in the top stack and update the result state if needed.
272     *
273     * @param resultState The result state to update if a height change of an child occurs
274     * @param algorithmState The state in which the current pass of the algorithm is currently in
275     *                       and which will be updated
276     */
277    private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
278            StackScrollAlgorithmState algorithmState) {
279
280        // The y Position if the element would be in a regular scrollView
281        float yPositionInScrollView = 0.0f;
282        ViewGroup hostView = resultState.getHostView();
283        int childCount = hostView.getChildCount();
284
285        // find the number of elements in the top stack.
286        for (int i = 0; i < childCount; i++) {
287            View child = hostView.getChildAt(i);
288            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
289            int childHeight = child.getHeight();
290            float yPositionInScrollViewAfterElement = yPositionInScrollView
291                    + childHeight
292                    + mPaddingBetweenElements;
293            if (yPositionInScrollView < algorithmState.scrollY) {
294                if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) {
295                    // According to the regular scroll view we are fully off screen
296                    algorithmState.itemsInTopStack += 1.0f;
297                    if (childHeight != mCollapsedSize) {
298                        childViewState.height = mCollapsedSize;
299                    }
300                } else {
301                    // According to the regular scroll view we are partially off screen
302                    // If it is expanded we have to collapse it to a new size
303                    float newSize = yPositionInScrollViewAfterElement
304                            - mPaddingBetweenElements
305                            - algorithmState.scrollY;
306
307                    // How much did we scroll into this child
308                    algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize
309                            + mPaddingBetweenElements);
310
311                    // Our element can be expanded, so this can get negative
312                    algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
313                    algorithmState.itemsInTopStack += algorithmState.partialInTop;
314                    // TODO: handle overlapping sizes with end stack
315                    newSize = Math.max(mCollapsedSize, newSize);
316                    // TODO: only temporarily collapse
317                    if (newSize != childHeight) {
318                        childViewState.height = (int) newSize;
319
320                        // We decrease scrollY by the same amount we made this child smaller.
321                        // The new scroll position is therefore the start of the element
322                        algorithmState.scrollY = (int) yPositionInScrollView;
323                        resultState.setScrollY(algorithmState.scrollY);
324                    }
325                    if (childHeight > mCollapsedSize) {
326                        // If we are just resizing this child, this element is not treated to be
327                        // transitioning into the stack and therefore it is the last element in
328                        // the stack.
329                        algorithmState.lastTopStackIndex = i;
330                        break;
331                    }
332                }
333            } else {
334                algorithmState.lastTopStackIndex = i;
335
336                // We are already past the stack so we can end the loop
337                break;
338            }
339            yPositionInScrollView = yPositionInScrollViewAfterElement;
340        }
341    }
342
343    /**
344     * Calculate the Z positions for all children based on the number of items in both stacks and
345     * save it in the resultState
346     *
347     * @param resultState The result state to update the zTranslation values
348     * @param algorithmState The state in which the current pass of the algorithm is currently in
349     */
350    private void updateZValuesForState(StackScrollState resultState,
351            StackScrollAlgorithmState algorithmState) {
352        ViewGroup hostView = resultState.getHostView();
353        int childCount = hostView.getChildCount();
354        for (int i = 0; i < childCount; i++) {
355            View child = hostView.getChildAt(i);
356            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
357            if (i < algorithmState.itemsInTopStack) {
358                float stackIndex = algorithmState.itemsInTopStack - i;
359                stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2);
360                childViewState.zTranslation = mZBasicHeight
361                        + stackIndex * mZDistanceBetweenElements;
362            } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
363                float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
364                float translationZ = mZBasicHeight
365                        - numItemsAbove * mZDistanceBetweenElements;
366                childViewState.zTranslation = translationZ;
367            } else {
368                childViewState.zTranslation = mZBasicHeight;
369            }
370        }
371    }
372
373    public float getLayoutHeight() {
374        return mLayoutHeight;
375    }
376
377    public void setLayoutHeight(float layoutHeight) {
378        this.mLayoutHeight = layoutHeight;
379    }
380
381    class StackScrollAlgorithmState {
382
383        /**
384         * The scroll position of the algorithm
385         */
386        public int scrollY;
387
388        /**
389         *  The quantity of items which are in the top stack.
390         */
391        public float itemsInTopStack;
392
393        /**
394         * how far in is the element currently transitioning into the top stack
395         */
396        public float partialInTop;
397
398        /**
399         * The last item index which is in the top stack.
400         * NOTE: In the top stack the item after the transitioning element is also in the stack!
401         * This is needed to ensure a smooth transition between the y position in the regular
402         * scrollview and the one in the stack.
403         */
404        public int lastTopStackIndex;
405
406        /**
407         * The quantity of items which are in the bottom stack.
408         */
409        public float itemsInBottomStack;
410
411        /**
412         * how far in is the element currently transitioning into the bottom stack
413         */
414        public float partialInBottom;
415    }
416
417}
418