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