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