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.content.res.Resources;
21import android.util.Log;
22import android.view.View;
23import android.view.ViewGroup;
24import com.android.systemui.R;
25import com.android.systemui.statusbar.EmptyShadeView;
26import com.android.systemui.statusbar.ExpandableNotificationRow;
27import com.android.systemui.statusbar.ExpandableView;
28import com.android.systemui.statusbar.FooterView;
29import com.android.systemui.statusbar.NotificationShelf;
30import com.android.systemui.statusbar.notification.NotificationUtils;
31
32import java.util.ArrayList;
33import java.util.HashMap;
34import java.util.List;
35
36/**
37 * The Algorithm of the {@link com.android.systemui.statusbar.stack
38 * .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
39 * .stack.StackScrollState}
40 */
41public class StackScrollAlgorithm {
42
43    private static final String LOG_TAG = "StackScrollAlgorithm";
44
45    private int mPaddingBetweenElements;
46    private int mIncreasedPaddingBetweenElements;
47    private int mCollapsedSize;
48
49    private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
50    private boolean mIsExpanded;
51    private boolean mClipNotificationScrollToTop;
52    private int mStatusBarHeight;
53    private float mHeadsUpInset;
54    private int mPinnedZTranslationExtra;
55
56    public StackScrollAlgorithm(Context context) {
57        initView(context);
58    }
59
60    public void initView(Context context) {
61        initConstants(context);
62    }
63
64    private void initConstants(Context context) {
65        Resources res = context.getResources();
66        mPaddingBetweenElements = res.getDimensionPixelSize(
67                R.dimen.notification_divider_height);
68        mIncreasedPaddingBetweenElements =
69                res.getDimensionPixelSize(R.dimen.notification_divider_height_increased);
70        mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
71        mStatusBarHeight = res.getDimensionPixelSize(R.dimen.status_bar_height);
72        mClipNotificationScrollToTop = res.getBoolean(R.bool.config_clipNotificationScrollToTop);
73        mHeadsUpInset = mStatusBarHeight + res.getDimensionPixelSize(
74                R.dimen.heads_up_status_bar_padding);
75        mPinnedZTranslationExtra = res.getDimensionPixelSize(
76                R.dimen.heads_up_pinned_elevation);
77    }
78
79    public void getStackScrollState(AmbientState ambientState, StackScrollState resultState) {
80        // The state of the local variables are saved in an algorithmState to easily subdivide it
81        // into multiple phases.
82        StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
83
84        // First we reset the view states to their default values.
85        resultState.resetViewStates();
86
87        initAlgorithmState(resultState, algorithmState, ambientState);
88
89        updatePositionsForState(resultState, algorithmState, ambientState);
90
91        updateZValuesForState(resultState, algorithmState, ambientState);
92
93        updateHeadsUpStates(resultState, algorithmState, ambientState);
94
95        handleDraggedViews(ambientState, resultState, algorithmState);
96        updateDimmedActivatedHideSensitive(ambientState, resultState, algorithmState);
97        updateClipping(resultState, algorithmState, ambientState);
98        updateSpeedBumpState(resultState, algorithmState, ambientState);
99        updateShelfState(resultState, ambientState);
100        getNotificationChildrenStates(resultState, algorithmState, ambientState);
101    }
102
103    private void getNotificationChildrenStates(StackScrollState resultState,
104            StackScrollAlgorithmState algorithmState,
105            AmbientState ambientState) {
106        int childCount = algorithmState.visibleChildren.size();
107        for (int i = 0; i < childCount; i++) {
108            ExpandableView v = algorithmState.visibleChildren.get(i);
109            if (v instanceof ExpandableNotificationRow) {
110                ExpandableNotificationRow row = (ExpandableNotificationRow) v;
111                row.getChildrenStates(resultState, ambientState);
112            }
113        }
114    }
115
116    private void updateSpeedBumpState(StackScrollState resultState,
117            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
118        int childCount = algorithmState.visibleChildren.size();
119        int belowSpeedBump = ambientState.getSpeedBumpIndex();
120        for (int i = 0; i < childCount; i++) {
121            View child = algorithmState.visibleChildren.get(i);
122            ExpandableViewState childViewState = resultState.getViewStateForView(child);
123
124            // The speed bump can also be gone, so equality needs to be taken when comparing
125            // indices.
126            childViewState.belowSpeedBump = i >= belowSpeedBump;
127        }
128
129    }
130    private void updateShelfState(StackScrollState resultState, AmbientState ambientState) {
131        NotificationShelf shelf = ambientState.getShelf();
132        if (shelf != null) {
133            shelf.updateState(resultState, ambientState);
134        }
135    }
136
137    private void updateClipping(StackScrollState resultState,
138            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
139        float drawStart = !ambientState.isOnKeyguard() ? ambientState.getTopPadding()
140                + ambientState.getStackTranslation() + ambientState.getExpandAnimationTopChange()
141                : 0;
142        float previousNotificationEnd = 0;
143        float previousNotificationStart = 0;
144        int childCount = algorithmState.visibleChildren.size();
145        for (int i = 0; i < childCount; i++) {
146            ExpandableView child = algorithmState.visibleChildren.get(i);
147            ExpandableViewState state = resultState.getViewStateForView(child);
148            if (!child.mustStayOnScreen() || state.headsUpIsVisible) {
149                previousNotificationEnd = Math.max(drawStart, previousNotificationEnd);
150                previousNotificationStart = Math.max(drawStart, previousNotificationStart);
151            }
152            float newYTranslation = state.yTranslation;
153            float newHeight = state.height;
154            float newNotificationEnd = newYTranslation + newHeight;
155            boolean isHeadsUp = (child instanceof ExpandableNotificationRow)
156                    && ((ExpandableNotificationRow) child).isPinned();
157            if (mClipNotificationScrollToTop
158                    && !state.inShelf && newYTranslation < previousNotificationEnd
159                    && (!isHeadsUp || ambientState.isShadeExpanded())) {
160                // The previous view is overlapping on top, clip!
161                float overlapAmount = previousNotificationEnd - newYTranslation;
162                state.clipTopAmount = (int) overlapAmount;
163            } else {
164                state.clipTopAmount = 0;
165            }
166
167            if (!child.isTransparent()) {
168                // Only update the previous values if we are not transparent,
169                // otherwise we would clip to a transparent view.
170                previousNotificationEnd = newNotificationEnd;
171                previousNotificationStart = newYTranslation;
172            }
173        }
174    }
175
176    public static boolean canChildBeDismissed(View v) {
177        if (!(v instanceof ExpandableNotificationRow)) {
178            return false;
179        }
180        ExpandableNotificationRow row = (ExpandableNotificationRow) v;
181        if (row.areGutsExposed()) {
182            return false;
183        }
184        return row.canViewBeDismissed();
185    }
186
187    /**
188     * Updates the dimmed, activated and hiding sensitive states of the children.
189     */
190    private void updateDimmedActivatedHideSensitive(AmbientState ambientState,
191            StackScrollState resultState, StackScrollAlgorithmState algorithmState) {
192        boolean dimmed = ambientState.isDimmed();
193        boolean dark = ambientState.isFullyDark();
194        boolean hideSensitive = ambientState.isHideSensitive();
195        View activatedChild = ambientState.getActivatedChild();
196        int childCount = algorithmState.visibleChildren.size();
197        for (int i = 0; i < childCount; i++) {
198            View child = algorithmState.visibleChildren.get(i);
199            ExpandableViewState childViewState = resultState.getViewStateForView(child);
200            childViewState.dimmed = dimmed;
201            childViewState.dark = dark;
202            childViewState.hideSensitive = hideSensitive;
203            boolean isActivatedChild = activatedChild == child;
204            if (dimmed && isActivatedChild) {
205                childViewState.zTranslation += 2.0f * ambientState.getZDistanceBetweenElements();
206            }
207        }
208    }
209
210    /**
211     * Handle the special state when views are being dragged
212     */
213    private void handleDraggedViews(AmbientState ambientState, StackScrollState resultState,
214            StackScrollAlgorithmState algorithmState) {
215        ArrayList<View> draggedViews = ambientState.getDraggedViews();
216        for (View draggedView : draggedViews) {
217            int childIndex = algorithmState.visibleChildren.indexOf(draggedView);
218            if (childIndex >= 0 && childIndex < algorithmState.visibleChildren.size() - 1) {
219                View nextChild = algorithmState.visibleChildren.get(childIndex + 1);
220                if (!draggedViews.contains(nextChild)) {
221                    // only if the view is not dragged itself we modify its state to be fully
222                    // visible
223                    ExpandableViewState viewState = resultState.getViewStateForView(
224                            nextChild);
225                    // The child below the dragged one must be fully visible
226                    if (ambientState.isShadeExpanded()) {
227                        viewState.shadowAlpha = 1;
228                        viewState.hidden = false;
229                    }
230                }
231
232                // Lets set the alpha to the one it currently has, as its currently being dragged
233                ExpandableViewState viewState = resultState.getViewStateForView(draggedView);
234                // The dragged child should keep the set alpha
235                viewState.alpha = draggedView.getAlpha();
236            }
237        }
238    }
239
240    /**
241     * Initialize the algorithm state like updating the visible children.
242     */
243    private void initAlgorithmState(StackScrollState resultState, StackScrollAlgorithmState state,
244            AmbientState ambientState) {
245        float bottomOverScroll = ambientState.getOverScrollAmount(false /* onTop */);
246
247        int scrollY = ambientState.getScrollY();
248
249        // Due to the overScroller, the stackscroller can have negative scroll state. This is
250        // already accounted for by the top padding and doesn't need an additional adaption
251        scrollY = Math.max(0, scrollY);
252        state.scrollY = (int) (scrollY + bottomOverScroll);
253
254        //now init the visible children and update paddings
255        ViewGroup hostView = resultState.getHostView();
256        int childCount = hostView.getChildCount();
257        state.visibleChildren.clear();
258        state.visibleChildren.ensureCapacity(childCount);
259        state.paddingMap.clear();
260        int notGoneIndex = 0;
261        ExpandableView lastView = null;
262        int firstHiddenIndex = ambientState.isDark()
263                ? (ambientState.hasPulsingNotifications() ? 1 : 0)
264                : childCount;
265
266        // The goal here is to fill the padding map, by iterating over how much padding each child
267        // needs. The map is thereby reused, by first filling it with the padding amount and when
268        // iterating over it again, it's filled with the actual resolved value.
269
270        for (int i = 0; i < childCount; i++) {
271            ExpandableView v = (ExpandableView) hostView.getChildAt(i);
272            if (v.getVisibility() != View.GONE) {
273                if (v == ambientState.getShelf()) {
274                    continue;
275                }
276                if (i >= firstHiddenIndex) {
277                    // we need normal padding now, to be in sync with what the stack calculates
278                    lastView = null;
279                }
280                notGoneIndex = updateNotGoneIndex(resultState, state, notGoneIndex, v);
281                float increasedPadding = v.getIncreasedPaddingAmount();
282                if (increasedPadding != 0.0f) {
283                    state.paddingMap.put(v, increasedPadding);
284                    if (lastView != null) {
285                        Float prevValue = state.paddingMap.get(lastView);
286                        float newValue = getPaddingForValue(increasedPadding);
287                        if (prevValue != null) {
288                            float prevPadding = getPaddingForValue(prevValue);
289                            if (increasedPadding > 0) {
290                                newValue = NotificationUtils.interpolate(
291                                        prevPadding,
292                                        newValue,
293                                        increasedPadding);
294                            } else if (prevValue > 0) {
295                                newValue = NotificationUtils.interpolate(
296                                        newValue,
297                                        prevPadding,
298                                        prevValue);
299                            }
300                        }
301                        state.paddingMap.put(lastView, newValue);
302                    }
303                } else if (lastView != null) {
304
305                    // Let's now resolve the value to an actual padding
306                    float newValue = getPaddingForValue(state.paddingMap.get(lastView));
307                    state.paddingMap.put(lastView, newValue);
308                }
309                if (v instanceof ExpandableNotificationRow) {
310                    ExpandableNotificationRow row = (ExpandableNotificationRow) v;
311
312                    // handle the notgoneIndex for the children as well
313                    List<ExpandableNotificationRow> children =
314                            row.getNotificationChildren();
315                    if (row.isSummaryWithChildren() && children != null) {
316                        for (ExpandableNotificationRow childRow : children) {
317                            if (childRow.getVisibility() != View.GONE) {
318                                ExpandableViewState childState
319                                        = resultState.getViewStateForView(childRow);
320                                childState.notGoneIndex = notGoneIndex;
321                                notGoneIndex++;
322                            }
323                        }
324                    }
325                }
326                lastView = v;
327            }
328        }
329        ExpandableNotificationRow expandingNotification = ambientState.getExpandingNotification();
330        state.indexOfExpandingNotification = expandingNotification != null
331                ? expandingNotification.isChildInGroup()
332                    ? state.visibleChildren.indexOf(expandingNotification.getNotificationParent())
333                    : state.visibleChildren.indexOf(expandingNotification)
334                : -1;
335    }
336
337    private float getPaddingForValue(Float increasedPadding) {
338        if (increasedPadding == null) {
339            return mPaddingBetweenElements;
340        } else if (increasedPadding >= 0.0f) {
341            return NotificationUtils.interpolate(
342                    mPaddingBetweenElements,
343                    mIncreasedPaddingBetweenElements,
344                    increasedPadding);
345        } else {
346            return NotificationUtils.interpolate(
347                    0,
348                    mPaddingBetweenElements,
349                    1.0f + increasedPadding);
350        }
351    }
352
353    private int updateNotGoneIndex(StackScrollState resultState,
354            StackScrollAlgorithmState state, int notGoneIndex,
355            ExpandableView v) {
356        ExpandableViewState viewState = resultState.getViewStateForView(v);
357        viewState.notGoneIndex = notGoneIndex;
358        state.visibleChildren.add(v);
359        notGoneIndex++;
360        return notGoneIndex;
361    }
362
363    /**
364     * Determine the positions for the views. This is the main part of the algorithm.
365     *
366     * @param resultState The result state to update if a change to the properties of a child occurs
367     * @param algorithmState The state in which the current pass of the algorithm is currently in
368     * @param ambientState The current ambient state
369     */
370    private void updatePositionsForState(StackScrollState resultState,
371            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
372
373        // The y coordinate of the current child.
374        float currentYPosition = -algorithmState.scrollY;
375        int childCount = algorithmState.visibleChildren.size();
376        for (int i = 0; i < childCount; i++) {
377            currentYPosition = updateChild(i, resultState, algorithmState, ambientState,
378                    currentYPosition);
379        }
380    }
381
382    protected float updateChild(int i, StackScrollState resultState,
383            StackScrollAlgorithmState algorithmState, AmbientState ambientState,
384            float currentYPosition) {
385        ExpandableView child = algorithmState.visibleChildren.get(i);
386        ExpandableViewState childViewState = resultState.getViewStateForView(child);
387        childViewState.location = ExpandableViewState.LOCATION_UNKNOWN;
388        int paddingAfterChild = getPaddingAfterChild(algorithmState, child);
389        int childHeight = getMaxAllowedChildHeight(child);
390        childViewState.yTranslation = currentYPosition;
391        boolean isFooterView = child instanceof FooterView;
392        boolean isEmptyShadeView = child instanceof EmptyShadeView;
393
394        childViewState.location = ExpandableViewState.LOCATION_MAIN_AREA;
395        float inset = ambientState.getTopPadding() + ambientState.getStackTranslation();
396        if (i <= algorithmState.getIndexOfExpandingNotification()) {
397            inset += ambientState.getExpandAnimationTopChange();
398        }
399        if (child.mustStayOnScreen() && childViewState.yTranslation >= 0) {
400            // Even if we're not scrolled away we're in view and we're also not in the
401            // shelf. We can relax the constraints and let us scroll off the top!
402            float end = childViewState.yTranslation + childViewState.height + inset;
403            childViewState.headsUpIsVisible = end < ambientState.getMaxHeadsUpTranslation();
404        }
405        if (isFooterView) {
406            childViewState.yTranslation = Math.min(childViewState.yTranslation,
407                    ambientState.getInnerHeight() - childHeight);
408        } else if (isEmptyShadeView) {
409            childViewState.yTranslation = ambientState.getInnerHeight() - childHeight
410                    + ambientState.getStackTranslation() * 0.25f;
411        } else {
412            clampPositionToShelf(child, childViewState, ambientState);
413        }
414
415        currentYPosition = childViewState.yTranslation + childHeight + paddingAfterChild;
416        if (currentYPosition <= 0) {
417            childViewState.location = ExpandableViewState.LOCATION_HIDDEN_TOP;
418        }
419        if (childViewState.location == ExpandableViewState.LOCATION_UNKNOWN) {
420            Log.wtf(LOG_TAG, "Failed to assign location for child " + i);
421        }
422
423        childViewState.yTranslation += inset;
424        return currentYPosition;
425    }
426
427    protected int getPaddingAfterChild(StackScrollAlgorithmState algorithmState,
428            ExpandableView child) {
429        return algorithmState.getPaddingAfterChild(child);
430    }
431
432    private void updateHeadsUpStates(StackScrollState resultState,
433            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
434        int childCount = algorithmState.visibleChildren.size();
435        ExpandableNotificationRow topHeadsUpEntry = null;
436        for (int i = 0; i < childCount; i++) {
437            View child = algorithmState.visibleChildren.get(i);
438            if (!(child instanceof ExpandableNotificationRow)) {
439                break;
440            }
441            ExpandableNotificationRow row = (ExpandableNotificationRow) child;
442            if (!row.isHeadsUp()) {
443                break;
444            }
445            ExpandableViewState childState = resultState.getViewStateForView(row);
446            if (topHeadsUpEntry == null && row.mustStayOnScreen() && !childState.headsUpIsVisible) {
447                topHeadsUpEntry = row;
448                childState.location = ExpandableViewState.LOCATION_FIRST_HUN;
449            }
450            boolean isTopEntry = topHeadsUpEntry == row;
451            float unmodifiedEndLocation = childState.yTranslation + childState.height;
452            if (mIsExpanded) {
453                if (row.mustStayOnScreen() && !childState.headsUpIsVisible) {
454                    // Ensure that the heads up is always visible even when scrolled off
455                    clampHunToTop(ambientState, row, childState);
456                    if (i == 0 && ambientState.isAboveShelf(row)) {
457                        // the first hun can't get off screen.
458                        clampHunToMaxTranslation(ambientState, row, childState);
459                        childState.hidden = false;
460                    }
461                }
462            }
463            if (row.isPinned()) {
464                childState.yTranslation = Math.max(childState.yTranslation, mHeadsUpInset);
465                childState.height = Math.max(row.getIntrinsicHeight(), childState.height);
466                childState.hidden = false;
467                ExpandableViewState topState = resultState.getViewStateForView(topHeadsUpEntry);
468                if (topState != null && !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            if (row.isHeadsUpAnimatingAway()) {
478                childState.hidden = false;
479            }
480        }
481    }
482
483    private void clampHunToTop(AmbientState ambientState, ExpandableNotificationRow row,
484            ExpandableViewState childState) {
485        float newTranslation = Math.max(ambientState.getTopPadding()
486                + ambientState.getStackTranslation(), childState.yTranslation);
487        childState.height = (int) Math.max(childState.height - (newTranslation
488                - childState.yTranslation), row.getCollapsedHeight());
489        childState.yTranslation = newTranslation;
490    }
491
492    private void clampHunToMaxTranslation(AmbientState ambientState, ExpandableNotificationRow row,
493            ExpandableViewState childState) {
494        float newTranslation;
495        float maxHeadsUpTranslation = ambientState.getMaxHeadsUpTranslation();
496        float maxShelfPosition = ambientState.getInnerHeight() + ambientState.getTopPadding()
497                + ambientState.getStackTranslation();
498        maxHeadsUpTranslation = Math.min(maxHeadsUpTranslation, maxShelfPosition);
499        float bottomPosition = maxHeadsUpTranslation - row.getCollapsedHeight();
500        newTranslation = Math.min(childState.yTranslation, bottomPosition);
501        childState.height = (int) Math.min(childState.height, maxHeadsUpTranslation
502                - newTranslation);
503        childState.yTranslation = newTranslation;
504    }
505
506    /**
507     * Clamp the height of the child down such that its end is at most on the beginning of
508     * the shelf.
509     *
510     * @param child
511     * @param childViewState the view state of the child
512     * @param ambientState the ambient state
513     */
514    private void clampPositionToShelf(ExpandableView child,
515            ExpandableViewState childViewState,
516            AmbientState ambientState) {
517        if (ambientState.getShelf() == null) {
518            return;
519        }
520
521        int shelfStart = ambientState.getInnerHeight()
522                - ambientState.getShelf().getIntrinsicHeight();
523        if (ambientState.isAppearing() && !child.isAboveShelf()) {
524            // Don't show none heads-up notifications while in appearing phase.
525            childViewState.yTranslation = Math.max(childViewState.yTranslation, shelfStart);
526        }
527        childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart);
528        if (childViewState.yTranslation >= shelfStart) {
529            childViewState.hidden = !child.isExpandAnimationRunning() && !child.hasExpandingChild();
530            childViewState.inShelf = true;
531            childViewState.headsUpIsVisible = false;
532        }
533    }
534
535    protected int getMaxAllowedChildHeight(View child) {
536        if (child instanceof ExpandableView) {
537            ExpandableView expandableView = (ExpandableView) child;
538            return expandableView.getIntrinsicHeight();
539        }
540        return child == null? mCollapsedSize : child.getHeight();
541    }
542
543    /**
544     * Calculate the Z positions for all children based on the number of items in both stacks and
545     * save it in the resultState
546     *  @param resultState The result state to update the zTranslation values
547     * @param algorithmState The state in which the current pass of the algorithm is currently in
548     * @param ambientState The ambient state of the algorithm
549     */
550    private void updateZValuesForState(StackScrollState resultState,
551            StackScrollAlgorithmState algorithmState, AmbientState ambientState) {
552        int childCount = algorithmState.visibleChildren.size();
553        float childrenOnTop = 0.0f;
554        for (int i = childCount - 1; i >= 0; i--) {
555            childrenOnTop = updateChildZValue(i, childrenOnTop,
556                    resultState, algorithmState, ambientState);
557        }
558    }
559
560    protected float updateChildZValue(int i, float childrenOnTop,
561            StackScrollState resultState, StackScrollAlgorithmState algorithmState,
562            AmbientState ambientState) {
563        ExpandableView child = algorithmState.visibleChildren.get(i);
564        ExpandableViewState childViewState = resultState.getViewStateForView(child);
565        int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements();
566        float baseZ = ambientState.getBaseZHeight();
567        if (child.mustStayOnScreen() && !childViewState.headsUpIsVisible
568                && !ambientState.isDozingAndNotPulsing(child)
569                && childViewState.yTranslation < ambientState.getTopPadding()
570                + ambientState.getStackTranslation()) {
571            if (childrenOnTop != 0.0f) {
572                childrenOnTop++;
573            } else {
574                float overlap = ambientState.getTopPadding()
575                        + ambientState.getStackTranslation() - childViewState.yTranslation;
576                childrenOnTop += Math.min(1.0f, overlap / childViewState.height);
577            }
578            childViewState.zTranslation = baseZ
579                    + childrenOnTop * zDistanceBetweenElements;
580        } else if (i == 0 && ambientState.isAboveShelf(child)) {
581            // In case this is a new view that has never been measured before, we don't want to
582            // elevate if we are currently expanded more then the notification
583            int shelfHeight = ambientState.getShelf() == null ? 0 :
584                    ambientState.getShelf().getIntrinsicHeight();
585            float shelfStart = ambientState.getInnerHeight()
586                    - shelfHeight + ambientState.getTopPadding()
587                    + ambientState.getStackTranslation();
588            float notificationEnd = childViewState.yTranslation + child.getPinnedHeadsUpHeight()
589                    + mPaddingBetweenElements;
590            if (shelfStart > notificationEnd) {
591                childViewState.zTranslation = baseZ;
592            } else {
593                float factor = (notificationEnd - shelfStart) / shelfHeight;
594                factor = Math.min(factor, 1.0f);
595                childViewState.zTranslation = baseZ + factor * zDistanceBetweenElements;
596            }
597        } else {
598            childViewState.zTranslation = baseZ;
599        }
600
601        // We need to scrim the notification more from its surrounding content when we are pinned,
602        // and we therefore elevate it higher.
603        // We can use the headerVisibleAmount for this, since the value nicely goes from 0 to 1 when
604        // expanding after which we have a normal elevation again.
605        childViewState.zTranslation += (1.0f - child.getHeaderVisibleAmount())
606                * mPinnedZTranslationExtra;
607        return childrenOnTop;
608    }
609
610    public void setIsExpanded(boolean isExpanded) {
611        this.mIsExpanded = isExpanded;
612    }
613
614    public class StackScrollAlgorithmState {
615
616        /**
617         * The scroll position of the algorithm
618         */
619        public int scrollY;
620
621        /**
622         * The children from the host view which are not gone.
623         */
624        public final ArrayList<ExpandableView> visibleChildren = new ArrayList<ExpandableView>();
625
626        /**
627         * The padding after each child measured in pixels.
628         */
629        public final HashMap<ExpandableView, Float> paddingMap = new HashMap<>();
630        private int indexOfExpandingNotification;
631
632        public int getPaddingAfterChild(ExpandableView child) {
633            Float padding = paddingMap.get(child);
634            if (padding == null) {
635                // Should only happen for the last view
636                return mPaddingBetweenElements;
637            }
638            return (int) padding.floatValue();
639        }
640
641        public int getIndexOfExpandingNotification() {
642            return indexOfExpandingNotification;
643        }
644    }
645
646}
647