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