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