StackScrollAlgorithm.java revision 42357e030c095867b95e2e8a718649587c5ebf52
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;
27import com.android.systemui.statusbar.notification.FakeShadowView;
28import com.android.systemui.statusbar.notification.NotificationUtils;
29
30import java.util.ArrayList;
31import java.util.HashMap;
32import java.util.List;
33
34/**
35 * The Algorithm of the {@link com.android.systemui.statusbar.stack
36 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
37 * .stack.StackScrollState}
38 */
39public class StackScrollAlgorithm {
40
41    private static final String LOG_TAG = "StackScrollAlgorithm";
42
43    private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
44
45    private int mPaddingBetweenElements;
46    private int mIncreasedPaddingBetweenElements;
47    private int mCollapsedSize;
48    private int mBottomStackPeekSize;
49    private int mZDistanceBetweenElements;
50    private int mZBasicHeight;
51
52    private StackIndentationFunctor mBottomStackIndentationFunctor;
53
54    private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
55    private boolean mIsExpansionChanging;
56    private int mFirstChildMaxHeight;
57    private boolean mIsExpanded;
58    private ExpandableView mFirstChildWhileExpanding;
59    private boolean mExpandedOnStart;
60    private int mBottomStackSlowDownLength;
61    private int mCollapseSecondCardPadding;
62
63    public StackScrollAlgorithm(Context context) {
64        initView(context);
65    }
66
67    public void initView(Context context) {
68        initConstants(context);
69    }
70
71    public int getBottomStackSlowDownLength() {
72        return mBottomStackSlowDownLength + mPaddingBetweenElements;
73    }
74
75    private void initConstants(Context context) {
76        mPaddingBetweenElements = Math.max(1, context.getResources()
77                .getDimensionPixelSize(R.dimen.notification_divider_height));
78        mIncreasedPaddingBetweenElements = context.getResources()
79                .getDimensionPixelSize(R.dimen.notification_divider_height_increased);
80        mCollapsedSize = context.getResources()
81                .getDimensionPixelSize(R.dimen.notification_min_height);
82        mBottomStackPeekSize = context.getResources()
83                .getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
84        mZDistanceBetweenElements = Math.max(1, context.getResources()
85                .getDimensionPixelSize(R.dimen.z_distance_between_notifications));
86        mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
87        mBottomStackSlowDownLength = context.getResources()
88                .getDimensionPixelSize(R.dimen.bottom_stack_slow_down_length);
89        mCollapseSecondCardPadding = context.getResources().getDimensionPixelSize(
90                R.dimen.notification_collapse_second_card_padding);
91        mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
92                MAX_ITEMS_IN_BOTTOM_STACK,
93                mBottomStackPeekSize,
94                getBottomStackSlowDownLength(),
95                0.5f);
96    }
97
98    public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
99        // The state of the local variables are saved in an algorithmState to easily subdivide it
100        // into multiple phases.
101        StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
102
103        // First we reset the view states to their default values.
104        resultState.resetViewStates();
105
106        initAlgorithmState(resultState, algorithmState, ambientState);
107
108        updatePositionsForState(resultState, algorithmState, ambientState);
109
110        updateZValuesForState(resultState, algorithmState, ambientState);
111
112        updateHeadsUpStates(resultState, algorithmState, ambientState);
113
114        handleDraggedViews(ambientState, resultState, algorithmState);
115        updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
116        updateClipping(resultState, algorithmState, ambientState);
117        updateSpeedBumpState(resultState, algorithmState, ambientState.getSpeedBumpIndex());
118        getNotificationChildrenStates(resultState, algorithmState);
119    }
120
121    private void getNotificationChildrenStates(StackScrollState resultState,
122            StackScrollAlgorithmState algorithmState) {
123        int childCount = algorithmState.visibleChildren.size();
124        for (int i = 0; i < childCount; i++) {
125            ExpandableView v = algorithmState.visibleChildren.get(i);
126            if (v instanceof ExpandableNotificationRow) {
127                ExpandableNotificationRow row = (ExpandableNotificationRow) v;
128                row.getChildrenStates(resultState);
129            }
130        }
131    }
132
133    private void updateSpeedBumpState(StackScrollState resultState,
134            StackScrollAlgorithmState algorithmState, int speedBumpIndex) {
135        int childCount = algorithmState.visibleChildren.size();
136        for (int i = 0; i < childCount; i++) {
137            View child = algorithmState.visibleChildren.get(i);
138            StackViewState childViewState = resultState.getViewStateForView(child);
139
140            // The speed bump can also be gone, so equality needs to be taken when comparing
141            // indices.
142            childViewState.belowSpeedBump = speedBumpIndex != -1 && i >= speedBumpIndex;
143        }
144    }
145
146    private void updateClipping(StackScrollState resultState,
147            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
148        boolean dismissAllInProgress = ambientState.isDismissAllInProgress();
149        float drawStart = ambientState.getTopPadding() + ambientState.getStackTranslation();
150        float previousNotificationEnd = 0;
151        float previousNotificationStart = 0;
152        boolean previousNotificationIsSwiped = false;
153        int childCount = algorithmState.visibleChildren.size();
154        for (int i = 0; i < childCount; i++) {
155            ExpandableView child = algorithmState.visibleChildren.get(i);
156            StackViewState state = resultState.getViewStateForView(child);
157            if (!child.mustStayOnScreen()) {
158                previousNotificationEnd = Math.max(drawStart, previousNotificationEnd);
159                previousNotificationStart = Math.max(drawStart, previousNotificationStart);
160            }
161            float newYTranslation = state.yTranslation;
162            float newHeight = state.height;
163            // apply clipping and shadow
164            float newNotificationEnd = newYTranslation + newHeight;
165
166            float clipHeight;
167            if (previousNotificationIsSwiped) {
168                // When the previous notification is swiped, we don't clip the content to the
169                // bottom of it.
170                clipHeight = newHeight;
171            } else {
172                clipHeight = newNotificationEnd - previousNotificationEnd;
173                clipHeight = Math.max(0.0f, clipHeight);
174            }
175
176            updateChildClippingAndBackground(state, newHeight, clipHeight,
177                    newHeight - (previousNotificationStart - newYTranslation));
178
179            if (dismissAllInProgress) {
180                state.clipTopAmount = Math.max(child.getMinClipTopAmount(), state.clipTopAmount);
181            }
182
183            if (!child.isTransparent()) {
184                // Only update the previous values if we are not transparent,
185                // otherwise we would clip to a transparent view.
186                if ((dismissAllInProgress && canChildBeDismissed(child))) {
187                    previousNotificationIsSwiped = true;
188                } else {
189                    previousNotificationIsSwiped = ambientState.getDraggedViews().contains(child);
190                    previousNotificationEnd = newNotificationEnd;
191                    previousNotificationStart =newYTranslation + state.clipTopAmount;
192                }
193            }
194        }
195    }
196
197    public static boolean canChildBeDismissed(View v) {
198        final View veto = v.findViewById(R.id.veto);
199        return (veto != null && veto.getVisibility() != View.GONE);
200    }
201
202    /**
203     * Updates the shadow outline and the clipping for a view.
204     *
205     * @param state the viewState to update
206     * @param realHeight the currently applied height of the view
207     * @param clipHeight the desired clip height, the rest of the view will be clipped from the top
208     * @param backgroundHeight the desired background height. The shadows of the view will be
209     *                         based on this height and the content will be clipped from the top
210     */
211    private void updateChildClippingAndBackground(StackViewState state, float realHeight,
212            float clipHeight, float backgroundHeight) {
213        if (realHeight > clipHeight) {
214            // Rather overlap than create a hole.
215            state.topOverLap = (int) Math.floor(realHeight - clipHeight);
216        } else {
217            state.topOverLap = 0;
218        }
219        if (realHeight > backgroundHeight) {
220            // Rather overlap than create a hole.
221            state.clipTopAmount = (int) Math.floor(realHeight - backgroundHeight);
222        } else {
223            state.clipTopAmount = 0;
224        }
225    }
226
227    /**
228     * Updates the dimmed, activated and hiding sensitive states of the children.
229     */
230    private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
231            StackScrollState resultState, StackScrollAlgorithmState algorithmState) {
232        boolean dimmed = ambientState.isDimmed();
233        boolean dark = ambientState.isDark();
234        boolean hideSensitive = ambientState.isHideSensitive();
235        View activatedChild = ambientState.getActivatedChild();
236        int childCount = algorithmState.visibleChildren.size();
237        for (int i = 0; i < childCount; i++) {
238            View child = algorithmState.visibleChildren.get(i);
239            StackViewState childViewState = resultState.getViewStateForView(child);
240            childViewState.dimmed = dimmed;
241            childViewState.dark = dark;
242            childViewState.hideSensitive = hideSensitive;
243            boolean isActivatedChild = activatedChild == child;
244            if (dimmed && isActivatedChild) {
245                childViewState.zTranslation += 2.0f * mZDistanceBetweenElements;
246            }
247        }
248    }
249
250    /**
251     * Handle the special state when views are being dragged
252     */
253    private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
254            StackScrollAlgorithmState algorithmState) {
255        ArrayList<View> draggedViews = ambientState.getDraggedViews();
256        for (View draggedView : draggedViews) {
257            int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
258            if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
259                View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
260                if (!draggedViews.contains(nextChild)) {
261                    // only if the view is not dragged itself we modify its state to be fully
262                    // visible
263                    StackViewState viewState = resultState.getViewStateForView(
264                            nextChild);
265                    // The child below the dragged one must be fully visible
266                    if (ambientState.isShadeExpanded()) {
267                        viewState.shadowAlpha = 1;
268                        viewState.hidden = false;
269                    }
270                }
271
272                // Lets set the alpha to the one it currently has, as its currently being dragged
273                StackViewState viewState = resultState.getViewStateForView(draggedView);
274                // The dragged child should keep the set alpha
275                viewState.alpha = draggedView.getAlpha();
276            }
277        }
278    }
279
280    /**
281     * Initialize the algorithm state like updating the visible children.
282     */
283    private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state,
284            AmbientState ambientState) {
285        state.itemsInBottomStack = 0.0f;
286        state.partialInBottom = 0.0f;
287        float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
288
289        int scrollY = ambientState.getScrollY();
290
291        // Due to the overScroller, the stackscroller can have negative scroll state. This is
292        // already accounted for by the top padding and doesn't need an additional adaption
293        scrollY = Math.max(0, scrollY);
294        state.scrollY = (int) (scrollY + bottomOverScroll);
295
296        //now init the visible children and update paddings
297        ViewGroup hostView = resultState.getHostView();
298        int childCount = hostView.getChildCount();
299        state.visibleChildren.clear();
300        state.visibleChildren.ensureCapacity(childCount);
301        state.increasedPaddingMap.clear();
302        int notGoneIndex = 0;
303        ExpandableView lastView = null;
304        for (int i = 0; i < childCount; i++) {
305            ExpandableView v = (ExpandableView) hostView.getChildAt(i);
306            if (v.getVisibility() != View.GONE) {
307                notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
308                float increasedPadding = v.getIncreasedPaddingAmount();
309                if (increasedPadding != 0.0f) {
310                    state.increasedPaddingMap.put(v, increasedPadding);
311                    if (lastView != null) {
312                        Float prevValue = state.increasedPaddingMap.get(lastView);
313                        float newValue = prevValue != null
314                                ? Math.max(prevValue, increasedPadding)
315                                : increasedPadding;
316                        state.increasedPaddingMap.put(lastView, newValue);
317                    }
318                }
319                if (v instanceof ExpandableNotificationRow) {
320                    ExpandableNotificationRow row = (ExpandableNotificationRow) v;
321
322                    // handle the notgoneIndex for the children as well
323                    List<ExpandableNotificationRow> children =
324                            row.getNotificationChildren();
325                    if (row.isSummaryWithChildren() && children != null) {
326                        for (ExpandableNotificationRow childRow : children) {
327                            if (childRow.getVisibility() != View.GONE) {
328                                StackViewState childState
329                                        = resultState.getViewStateForView(childRow);
330                                childState.notGoneIndex = notGoneIndex;
331                                notGoneIndex++;
332                            }
333                        }
334                    }
335                }
336                lastView = v;
337            }
338        }
339    }
340
341    private int updateNotGoneIndex(StackScrollState resultState,
342            StackScrollAlgorithmState state, int notGoneIndex,
343            ExpandableView v) {
344        StackViewState viewState = resultState.getViewStateForView(v);
345        viewState.notGoneIndex = notGoneIndex;
346        state.visibleChildren.add(v);
347        notGoneIndex++;
348        return notGoneIndex;
349    }
350
351    /**
352     * Determine the positions for the views. This is the main part of the algorithm.
353     *
354     * @param resultState The result state to update if a change to the properties of a child occurs
355     * @param algorithmState The state in which the current pass of the algorithm is currently in
356     * @param ambientState The current ambient state
357     */
358    private void updatePositionsForState(StackScrollState resultState,
359            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
360
361        // The starting position of the bottom stack peek
362        float bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize;
363
364        // The position where the bottom stack starts.
365        float bottomStackStart = bottomPeekStart - mBottomStackSlowDownLength;
366
367        // The y coordinate of the current child.
368        float currentYPosition = -algorithmState.scrollY;
369
370        int childCount = algorithmState.visibleChildren.size();
371        int paddingAfterChild;
372        for (int i = 0; i < childCount; i++) {
373            ExpandableView child = algorithmState.visibleChildren.get(i);
374            StackViewState childViewState = resultState.getViewStateForView(child);
375            childViewState.location = StackViewState.LOCATION_UNKNOWN;
376            paddingAfterChild = getPaddingAfterChild(algorithmState, child);
377            int childHeight = getMaxAllowedChildHeight(child);
378            int minHeight = child.getMinHeight();
379            childViewState.yTranslation = currentYPosition;
380            if (i == 0) {
381                updateFirstChildHeight(child, childViewState, childHeight, ambientState);
382            }
383
384            // The y position after this element
385            float nextYPosition = currentYPosition + childHeight +
386                    paddingAfterChild;
387            if (nextYPosition >= bottomStackStart) {
388                // Case 1:
389                // We are in the bottom stack.
390                if (currentYPosition >= bottomStackStart) {
391                    // According to the regular scroll view we are fully translated out of the
392                    // bottom of the screen so we are fully in the bottom stack
393                    updateStateForChildFullyInBottomStack(algorithmState,
394                            bottomStackStart, childViewState, minHeight, ambientState, child);
395                } else {
396                    // According to the regular scroll view we are currently translating out of /
397                    // into the bottom of the screen
398                    updateStateForChildTransitioningInBottom(algorithmState,
399                            bottomStackStart, child, currentYPosition,
400                            childViewState, childHeight);
401                }
402            } else {
403                // Case 2:
404                // We are in the regular scroll area.
405                childViewState.location = StackViewState.LOCATION_MAIN_AREA;
406                clampPositionToBottomStackStart(childViewState, childViewState.height, childHeight,
407                        ambientState);
408            }
409
410            if (i == 0 && ambientState.getScrollY() <= 0) {
411                // The first card can get into the bottom stack if it's the only one
412                // on the lockscreen which pushes it up. Let's make sure that doesn't happen and
413                // it stays at the top
414                childViewState.yTranslation = Math.max(0, childViewState.yTranslation);
415            }
416            currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild;
417            if (currentYPosition <= 0) {
418                childViewState.location = StackViewState.LOCATION_HIDDEN_TOP;
419            }
420            if (childViewState.location == StackViewState.LOCATION_UNKNOWN) {
421                Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
422            }
423
424            childViewState.yTranslation += ambientState.getTopPadding()
425                    + ambientState.getStackTranslation();
426        }
427    }
428
429    private int getPaddingAfterChild(StackScrollAlgorithmState algorithmState,
430            ExpandableView child) {
431        Float paddingValue = algorithmState.increasedPaddingMap.get(child);
432        return paddingValue == null
433                ? mPaddingBetweenElements
434                : (int) NotificationUtils.interpolate(mPaddingBetweenElements,
435                        mIncreasedPaddingBetweenElements,
436                        paddingValue);
437    }
438
439    private void updateHeadsUpStates(StackScrollState resultState,
440            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
441        int childCount = algorithmState.visibleChildren.size();
442        ExpandableNotificationRow topHeadsUpEntry = null;
443        for (int i = 0; i < childCount; i++) {
444            View child = algorithmState.visibleChildren.get(i);
445            if (!(child instanceof ExpandableNotificationRow)) {
446                break;
447            }
448            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
449            if (!row.isHeadsUp()) {
450                break;
451            }
452            StackViewState childState = resultState.getViewStateForView(row);
453            if (topHeadsUpEntry == null) {
454                topHeadsUpEntry = row;
455                childState.location = StackViewState.LOCATION_FIRST_HUN;
456            }
457            boolean isTopEntry = topHeadsUpEntry == row;
458            float unmodifiedEndLocation = childState.yTranslation + childState.height;
459            if (mIsExpanded) {
460                // Ensure that the heads up is always visible even when scrolled off
461                clampHunToTop(ambientState, row, childState);
462                clampHunToMaxTranslation(ambientState, row, childState);
463            }
464            if (row.isPinned()) {
465                childState.yTranslation = Math.max(childState.yTranslation, 0);
466                childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
467                StackViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
468                if (!isTopEntry && (!mIsExpanded
469                        || unmodifiedEndLocation < topState.yTranslation + topState.height)) {
470                    // Ensure that a headsUp doesn't vertically extend further than the heads-up at
471                    // the top most z-position
472                    childState.height = row.getIntrinsicHeight();
473                    childState.yTranslation = topState.yTranslation + topState.height
474                            - childState.height;
475                }
476            }
477        }
478    }
479
480    private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
481            StackViewState childState) {
482        float newTranslation = Math.max(ambientState.getTopPadding()
483                + ambientState.getStackTranslation(), childState.yTranslation);
484        childState.height = (int) Math.max(childState.height - (newTranslation
485                - childState.yTranslation), row.getMinHeight());
486        childState.yTranslation = newTranslation;
487    }
488
489    private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
490            StackViewState childState) {
491        float newTranslation;
492        float bottomPosition = ambientState.getMaxHeadsUpTranslation() - row.getMinHeight();
493        newTranslation = Math.min(childState.yTranslation, bottomPosition);
494        childState.height = (int) Math.max(childState.height
495                - (childState.yTranslation - newTranslation), row.getMinHeight());
496        childState.yTranslation = newTranslation;
497    }
498
499    /**
500     * Clamp the yTranslation of the child down such that its end is at most on the beginning of
501     * the bottom stack.
502     *
503     * @param childViewState the view state of the child
504     * @param childHeight the height of this child
505     * @param minHeight the minumum Height of the View
506     */
507    private void clampPositionToBottomStackStart(StackViewState childViewState,
508            int childHeight, int minHeight, AmbientState ambientState) {
509
510        int bottomStackStart = ambientState.getInnerHeight()
511                - mBottomStackPeekSize - mCollapseSecondCardPadding;
512        int childStart = bottomStackStart - childHeight;
513        if (childStart < childViewState.yTranslation) {
514            float newHeight = bottomStackStart - childViewState.yTranslation;
515            if (newHeight < minHeight) {
516                newHeight = minHeight;
517                childViewState.yTranslation = bottomStackStart - minHeight;
518            }
519            childViewState.height = (int) newHeight;
520        }
521    }
522
523    private int getMaxAllowedChildHeight(View child) {
524        if (child instanceof ExpandableView) {
525            ExpandableView expandableView = (ExpandableView) child;
526            return expandableView.getIntrinsicHeight();
527        }
528        return child == null? mCollapsedSize : child.getHeight();
529    }
530
531    private void updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
532            float transitioningPositionStart, ExpandableView child, float currentYPosition,
533            StackViewState childViewState, int childHeight) {
534
535        // This is the transitioning element on top of bottom stack, calculate how far we are in.
536        algorithmState.partialInBottom = 1.0f - (
537                (transitioningPositionStart - currentYPosition) / (childHeight +
538                        getPaddingAfterChild(algorithmState, child)));
539
540        // the offset starting at the transitionPosition of the bottom stack
541        float offset = mBottomStackIndentationFunctor.getValue(algorithmState.partialInBottom);
542        algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
543        int newHeight = childHeight;
544        if (childHeight > child.getMinHeight()) {
545            newHeight = (int) Math.max(Math.min(transitioningPositionStart + offset -
546                    getPaddingAfterChild(algorithmState, child) - currentYPosition, childHeight),
547                    child.getMinHeight());
548            childViewState.height = newHeight;
549        }
550        childViewState.yTranslation = transitioningPositionStart + offset - newHeight
551                - getPaddingAfterChild(algorithmState, child);
552        childViewState.location = StackViewState.LOCATION_MAIN_AREA;
553    }
554
555    private void updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
556            float transitioningPositionStart, StackViewState childViewState,
557            int minHeight, AmbientState ambientState, ExpandableView child) {
558        float currentYPosition;
559        algorithmState.itemsInBottomStack += 1.0f;
560        if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
561            // We are visually entering the bottom stack
562            currentYPosition = transitioningPositionStart
563                    + mBottomStackIndentationFunctor.getValue(algorithmState.itemsInBottomStack)
564                    - getPaddingAfterChild(algorithmState, child);
565            childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_PEEKING;
566        } else {
567            // we are fully inside the stack
568            if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
569                childViewState.hidden = true;
570                childViewState.shadowAlpha = 0.0f;
571            } else if (algorithmState.itemsInBottomStack
572                    > MAX_ITEMS_IN_BOTTOM_STACK + 1) {
573                childViewState.shadowAlpha = 1.0f - algorithmState.partialInBottom;
574            }
575            childViewState.location = StackViewState.LOCATION_BOTTOM_STACK_HIDDEN;
576            currentYPosition = ambientState.getInnerHeight();
577        }
578        childViewState.height = minHeight;
579        childViewState.yTranslation = currentYPosition - minHeight;
580    }
581
582
583    /**
584     * Update the height of the first child i.e clamp it to the bottom stack
585     *
586     *
587
588     * @param child the child to update
589     * @param childViewState the viewstate of the child
590     * @param childHeight the height of the child
591     * @param ambientState The ambient state of the algorithm
592     */
593    private void updateFirstChildHeight(ExpandableView child, StackViewState childViewState,
594            int childHeight, AmbientState ambientState) {
595
596            // The starting position of the bottom stack peek
597            int bottomPeekStart = ambientState.getInnerHeight() - mBottomStackPeekSize -
598                    mCollapseSecondCardPadding + ambientState.getScrollY();
599            // Collapse and expand the first child while the shade is being expanded
600            float maxHeight = mIsExpansionChanging && child == mFirstChildWhileExpanding
601                    ? mFirstChildMaxHeight
602                    : childHeight;
603            childViewState.height = (int) Math.max(Math.min(bottomPeekStart, maxHeight),
604                    child.getMinHeight());
605    }
606
607    /**
608     * Calculate the Z positions for all children based on the number of items in both stacks and
609     * save it in the resultState
610     *  @param resultState The result state to update the zTranslation values
611     * @param algorithmState The state in which the current pass of the algorithm is currently in
612     * @param ambientState The ambient state of the algorithm
613     */
614    private void updateZValuesForState(StackScrollState resultState,
615            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
616        int childCount = algorithmState.visibleChildren.size();
617        float childrenOnTop = 0.0f;
618        for (int i = childCount - 1; i >= 0; i--) {
619            ExpandableView child = algorithmState.visibleChildren.get(i);
620            StackViewState childViewState = resultState.getViewStateForView(child);
621            if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
622                // We are in the bottom stack
623                float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
624                float zSubtraction;
625                if (numItemsAbove <= 1.0f) {
626                    float factor = 0.2f;
627                    // Lets fade in slower to the threshold to make the shadow fade in look nicer
628                    if (numItemsAbove <= factor) {
629                        zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD
630                                * numItemsAbove * (1.0f / factor);
631                    } else {
632                        zSubtraction = FakeShadowView.SHADOW_SIBLING_TRESHOLD
633                                + (numItemsAbove - factor) * (1.0f / (1.0f - factor))
634                                        * (mZDistanceBetweenElements
635                                                - FakeShadowView.SHADOW_SIBLING_TRESHOLD);
636                    }
637                } else {
638                    zSubtraction = numItemsAbove * mZDistanceBetweenElements;
639                }
640                childViewState.zTranslation = mZBasicHeight - zSubtraction;
641            } else if (child.mustStayOnScreen()
642                    && childViewState.yTranslation < ambientState.getTopPadding()
643                    + ambientState.getStackTranslation()) {
644                if (childrenOnTop != 0.0f) {
645                    childrenOnTop++;
646                } else {
647                    float overlap = ambientState.getTopPadding()
648                            + ambientState.getStackTranslation() - childViewState.yTranslation;
649                    childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
650                }
651                childViewState.zTranslation = mZBasicHeight
652                        + childrenOnTop * mZDistanceBetweenElements;
653            } else {
654                childViewState.zTranslation = mZBasicHeight;
655            }
656        }
657    }
658
659    public void onExpansionStarted(StackScrollState currentState) {
660        mIsExpansionChanging = true;
661        mExpandedOnStart = mIsExpanded;
662        ViewGroup hostView = currentState.getHostView();
663        updateFirstChildHeightWhileExpanding(hostView);
664    }
665
666    private void updateFirstChildHeightWhileExpanding(ViewGroup hostView) {
667        mFirstChildWhileExpanding = (ExpandableView) findFirstVisibleChild(hostView);
668        if (mFirstChildWhileExpanding != null) {
669            if (mExpandedOnStart) {
670
671                // We are collapsing the shade, so the first child can get as most as high as the
672                // current height or the end value of the animation.
673                mFirstChildMaxHeight = StackStateAnimator.getFinalActualHeight(
674                        mFirstChildWhileExpanding);
675            } else {
676                updateFirstChildMaxSizeToMaxHeight();
677            }
678        } else {
679            mFirstChildMaxHeight = 0;
680        }
681    }
682
683    private void updateFirstChildMaxSizeToMaxHeight() {
684        // We are expanding the shade, expand it to its full height.
685        if (!isMaxSizeInitialized(mFirstChildWhileExpanding)) {
686
687            // This child was not layouted yet, wait for a layout pass
688            mFirstChildWhileExpanding
689                    .addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
690                        @Override
691                        public void onLayoutChange(View v, int left, int top, int right,
692                                int bottom, int oldLeft, int oldTop, int oldRight,
693                                int oldBottom) {
694                            if (mFirstChildWhileExpanding != null) {
695                                mFirstChildMaxHeight = getMaxAllowedChildHeight(
696                                        mFirstChildWhileExpanding);
697                            } else {
698                                mFirstChildMaxHeight = 0;
699                            }
700                            v.removeOnLayoutChangeListener(this);
701                        }
702                    });
703        } else {
704            mFirstChildMaxHeight = getMaxAllowedChildHeight(mFirstChildWhileExpanding);
705        }
706    }
707
708    private boolean isMaxSizeInitialized(ExpandableView child) {
709        if (child instanceof ExpandableNotificationRow) {
710            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
711            return row.isMaxExpandHeightInitialized();
712        }
713        return child == null || child.getWidth() != 0;
714    }
715
716    private View findFirstVisibleChild(ViewGroup container) {
717        int childCount = container.getChildCount();
718        for (int i = 0; i < childCount; i++) {
719            View child = container.getChildAt(i);
720            if (child.getVisibility() != View.GONE) {
721                return child;
722            }
723        }
724        return null;
725    }
726
727    public void onExpansionStopped() {
728        mIsExpansionChanging = false;
729        mFirstChildWhileExpanding = null;
730    }
731
732    public void setIsExpanded(boolean isExpanded) {
733        this.mIsExpanded = isExpanded;
734    }
735
736    public void notifyChildrenChanged(final NotificationStackScrollLayout hostView) {
737        if (mIsExpansionChanging) {
738            hostView.post(new Runnable() {
739                @Override
740                public void run() {
741                    updateFirstChildHeightWhileExpanding(hostView);
742                }
743            });
744        }
745    }
746
747    public void onReset(ExpandableView view) {
748        if (view.equals(mFirstChildWhileExpanding)) {
749            updateFirstChildMaxSizeToMaxHeight();
750        }
751    }
752
753    class StackScrollAlgorithmState {
754
755        /**
756         * The scroll position of the algorithm
757         */
758        public int scrollY;
759
760        /**
761         * The quantity of items which are in the bottom stack.
762         */
763        public float itemsInBottomStack;
764
765        /**
766         * how far in is the element currently transitioning into the bottom stack
767         */
768        public float partialInBottom;
769
770        /**
771         * The children from the host view which are not gone.
772         */
773        public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
774
775        /**
776         * The children from the host that need an increased padding after them. A value of 0 means
777         * no increased padding, a value of 1 means full padding.
778         */
779        public final HashMap<ExpandableView, Float> increasedPaddingMap = new HashMap<>();
780    }
781
782}
783