StackScrollAlgorithm.java revision 584a7aa62c54bcbd654a6696d4fbb56e124874e7
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        ViewGroup hostView = resultState.getHostView();
419        int childCount = algorithmState.visibleChildren.size();
420        for (int i = 0; i < childCount; i++) {
421            View child = algorithmState.visibleChildren.get(i);
422            if (child.getVisibility() == View.GONE) continue;
423            StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
424            if (i < algorithmState.itemsInTopStack) {
425                float stackIndex = algorithmState.itemsInTopStack - i;
426                stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2);
427                childViewState.zTranslation = mZBasicHeight
428                        + stackIndex * mZDistanceBetweenElements;
429            } else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
430                float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
431                float translationZ = mZBasicHeight
432                        - numItemsAbove * mZDistanceBetweenElements;
433                childViewState.zTranslation = translationZ;
434            } else {
435                childViewState.zTranslation = mZBasicHeight;
436            }
437        }
438    }
439
440    public float getLayoutHeight() {
441        return mLayoutHeight;
442    }
443
444    public void setLayoutHeight(float layoutHeight) {
445        this.mLayoutHeight = layoutHeight;
446    }
447
448    public void onExpansionStarted(StackScrollState currentState) {
449        mIsExpansionChanging = true;
450        mExpandedOnStart = mIsExpanded;
451        ViewGroup hostView = currentState.getHostView();
452        updateFirstChildHeightWhileExpanding(hostView);
453    }
454
455    private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) {
456        if (hostView.getChildCount() > 0) {
457            mFirstChildWhileExpanding = hostView.getChildAt(0);
458            if (mExpandedOnStart) {
459
460                // We are collapsing the shade, so the first child can get as most as high as the
461                // current height.
462                mFirstChildMaxHeight = mFirstChildWhileExpanding.getHeight();
463            } else {
464
465                // We are expanding the shade, expand it to its full height.
466                mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding);
467            }
468        } else {
469            mFirstChildWhileExpanding = null;
470            mFirstChildMaxHeight = 0;
471        }
472    }
473
474    public void onExpansionStopped() {
475        mIsExpansionChanging = false;
476        mFirstChildWhileExpanding = null;
477    }
478
479    public void setIsExpanded(boolean isExpanded) {
480        this.mIsExpanded = isExpanded;
481    }
482
483    public void notifyChildrenChanged(ViewGroup hostView) {
484        if (mIsExpansionChanging) {
485            updateFirstChildHeightWhileExpanding(hostView);
486        }
487    }
488
489    class StackScrollAlgorithmState {
490
491        /**
492         * The scroll position of the algorithm
493         */
494        public int scrollY;
495
496        /**
497         *  The quantity of items which are in the top stack.
498         */
499        public float itemsInTopStack;
500
501        /**
502         * how far in is the element currently transitioning into the top stack
503         */
504        public float partialInTop;
505
506        /**
507         * The last item index which is in the top stack.
508         * NOTE: In the top stack the item after the transitioning element is also in the stack!
509         * This is needed to ensure a smooth transition between the y position in the regular
510         * scrollview and the one in the stack.
511         */
512        public int lastTopStackIndex;
513
514        /**
515         * The quantity of items which are in the bottom stack.
516         */
517        public float itemsInBottomStack;
518
519        /**
520         * how far in is the element currently transitioning into the bottom stack
521         */
522        public float partialInBottom;
523
524        /**
525         * The children from the host view which are not gone.
526         */
527        public final ArrayList<View> visibleChildren = new ArrayList<View>();
528    }
529
530}
531