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