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