NotificationChildrenContainer.java revision e812652fba04abc32fd6eeefee3463270f3b9d8e
1/*
2 * Copyright (C) 2015 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.AttributeSet;
21import android.view.LayoutInflater;
22import android.view.View;
23import android.view.ViewGroup;
24
25import com.android.systemui.R;
26import com.android.systemui.ViewInvertHelper;
27import com.android.systemui.statusbar.ExpandableNotificationRow;
28import com.android.systemui.statusbar.notification.HybridNotificationView;
29import com.android.systemui.statusbar.notification.HybridNotificationViewManager;
30import com.android.systemui.statusbar.phone.NotificationPanelView;
31
32import java.util.ArrayList;
33import java.util.List;
34
35/**
36 * A container containing child notifications
37 */
38public class NotificationChildrenContainer extends ViewGroup {
39
40    private static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2;
41    private static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5;
42    private static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8;
43
44    private final int mChildPadding;
45    private final int mDividerHeight;
46    private final int mMaxNotificationHeight;
47    private final List<View> mDividers = new ArrayList<>();
48    private final List<ExpandableNotificationRow> mChildren = new ArrayList<>();
49    private final int mNotificationHeaderHeight;
50    private final int mNotificationAppearDistance;
51    private final int mNotificatonTopPadding;
52    private final HybridNotificationViewManager mHybridViewManager;
53    private final float mCollapsedBottompadding;
54    private ViewInvertHelper mOverflowInvertHelper;
55    private boolean mChildrenExpanded;
56    private ExpandableNotificationRow mNotificationParent;
57    private HybridNotificationView mGroupOverflowContainer;
58    private ViewState mGroupOverFlowState;
59
60    public NotificationChildrenContainer(Context context) {
61        this(context, null);
62    }
63
64    public NotificationChildrenContainer(Context context, AttributeSet attrs) {
65        this(context, attrs, 0);
66    }
67
68    public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) {
69        this(context, attrs, defStyleAttr, 0);
70    }
71
72    public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
73            int defStyleRes) {
74        super(context, attrs, defStyleAttr, defStyleRes);
75        mChildPadding = getResources().getDimensionPixelSize(
76                R.dimen.notification_children_padding);
77        mDividerHeight = getResources().getDimensionPixelSize(
78                R.dimen.notification_children_divider_height);
79        mMaxNotificationHeight = getResources().getDimensionPixelSize(
80                R.dimen.notification_max_height);
81        mNotificationAppearDistance = getResources().getDimensionPixelSize(
82                R.dimen.notification_appear_distance);
83        mNotificationHeaderHeight = getResources().getDimensionPixelSize(
84                com.android.internal.R.dimen.notification_content_margin_top);
85        mNotificatonTopPadding = getResources().getDimensionPixelSize(
86                R.dimen.notification_children_container_top_padding);
87        mCollapsedBottompadding = 11.5f * getResources().getDisplayMetrics().density;
88        mHybridViewManager = new HybridNotificationViewManager(getContext(), this);
89    }
90
91    @Override
92    protected void onLayout(boolean changed, int l, int t, int r, int b) {
93        int childCount = mChildren.size();
94        for (int i = 0; i < childCount; i++) {
95            View child = mChildren.get(i);
96            if (child.getVisibility() == View.GONE) {
97                continue;
98            }
99            child.layout(0, 0, getWidth(), child.getMeasuredHeight());
100            mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
101        }
102        if (mGroupOverflowContainer != null) {
103            mGroupOverflowContainer.layout(0, 0, getWidth(),
104                    mGroupOverflowContainer.getMeasuredHeight());
105        }
106    }
107
108    @Override
109    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
110        int ownMaxHeight = mMaxNotificationHeight;
111        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
112        boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
113        boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
114        if (hasFixedHeight || isHeightLimited) {
115            int size = MeasureSpec.getSize(heightMeasureSpec);
116            ownMaxHeight = Math.min(ownMaxHeight, size);
117        }
118        int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST);
119        int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
120        int height = mNotificationHeaderHeight + mNotificatonTopPadding;
121        int childCount = mChildren.size();
122        for (int i = 0; i < childCount; i++) {
123            View child = mChildren.get(i);
124            child.measure(widthMeasureSpec, newHeightSpec);
125            height += child.getMeasuredHeight();
126
127            // layout the divider
128            View divider = mDividers.get(i);
129            divider.measure(widthMeasureSpec, dividerHeightSpec);
130            height += mDividerHeight;
131        }
132        int width = MeasureSpec.getSize(widthMeasureSpec);
133        if (mGroupOverflowContainer != null) {
134            mGroupOverflowContainer.measure(widthMeasureSpec, newHeightSpec);
135        }
136        setMeasuredDimension(width, height);
137    }
138
139    /**
140     * Add a child notification to this view.
141     *
142     * @param row the row to add
143     * @param childIndex the index to add it at, if -1 it will be added at the end
144     */
145    public void addNotification(ExpandableNotificationRow row, int childIndex) {
146        int newIndex = childIndex < 0 ? mChildren.size() : childIndex;
147        mChildren.add(newIndex, row);
148        addView(row);
149
150        View divider = inflateDivider();
151        addView(divider);
152        mDividers.add(newIndex, divider);
153
154        updateGroupOverflow();
155    }
156
157    public void removeNotification(ExpandableNotificationRow row) {
158        int childIndex = mChildren.indexOf(row);
159        mChildren.remove(row);
160        removeView(row);
161
162        View divider = mDividers.remove(childIndex);
163        removeView(divider);
164
165        row.setSystemChildExpanded(false);
166        updateGroupOverflow();
167    }
168
169    public void updateGroupOverflow() {
170        int childCount = mChildren.size();
171        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
172        boolean hasOverflow = childCount > maxAllowedVisibleChildren;
173        int lastVisibleIndex = hasOverflow ? maxAllowedVisibleChildren - 2
174                : maxAllowedVisibleChildren - 1;
175        if (hasOverflow) {
176            mGroupOverflowContainer = mHybridViewManager.bindFromNotificationGroup(
177                    mGroupOverflowContainer, mChildren, lastVisibleIndex + 1);
178            if (mOverflowInvertHelper == null) {
179                mOverflowInvertHelper= new ViewInvertHelper(mGroupOverflowContainer,
180                        NotificationPanelView.DOZE_ANIMATION_DURATION);
181            }
182            if (mGroupOverFlowState == null) {
183                mGroupOverFlowState = new ViewState();
184            }
185        } else if (mGroupOverflowContainer != null) {
186            removeView(mGroupOverflowContainer);
187            mGroupOverflowContainer = null;
188            mOverflowInvertHelper = null;
189            mGroupOverFlowState = null;
190        }
191    }
192
193    private View inflateDivider() {
194        return LayoutInflater.from(mContext).inflate(
195                R.layout.notification_children_divider, this, false);
196    }
197
198    public List<ExpandableNotificationRow> getNotificationChildren() {
199        return mChildren;
200    }
201
202    /**
203     * Apply the order given in the list to the children.
204     *
205     * @param childOrder the new list order
206     * @return whether the list order has changed
207     */
208    public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) {
209        if (childOrder == null) {
210            return false;
211        }
212        boolean result = false;
213        for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) {
214            ExpandableNotificationRow child = mChildren.get(i);
215            ExpandableNotificationRow desiredChild = childOrder.get(i);
216            if (child != desiredChild) {
217                mChildren.remove(desiredChild);
218                mChildren.add(i, desiredChild);
219                result = true;
220            }
221        }
222        updateExpansionStates();
223        return result;
224    }
225
226    private void updateExpansionStates() {
227        // Let's make the first child expanded if the parent is
228        for (int i = 0; i < mChildren.size(); i++) {
229            ExpandableNotificationRow child = mChildren.get(i);
230            child.setSystemChildExpanded(false);
231        }
232    }
233
234    /**
235     *
236     * @return the intrinsic size of this children container, i.e the natural fully expanded state
237     */
238    public int getIntrinsicHeight() {
239        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
240        return getIntrinsicHeight(maxAllowedVisibleChildren);
241    }
242
243    /**
244     * @return the intrinsic height with a number of children given
245     *         in @param maxAllowedVisibleChildren
246     */
247    private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
248        int intrinsicHeight = mNotificationHeaderHeight;
249        if (mChildrenExpanded) {
250            intrinsicHeight += mNotificatonTopPadding;
251        }
252        int visibleChildren = 0;
253        int childCount = mChildren.size();
254        for (int i = 0; i < childCount; i++) {
255            if (visibleChildren >= maxAllowedVisibleChildren) {
256                break;
257            }
258            ExpandableNotificationRow child = mChildren.get(i);
259            intrinsicHeight += child.getIntrinsicHeight();
260            visibleChildren++;
261        }
262        if (visibleChildren > 0) {
263            if (mChildrenExpanded) {
264                intrinsicHeight += visibleChildren * mDividerHeight;
265            } else {
266                intrinsicHeight += (visibleChildren - 1) * mChildPadding;
267            }
268        }
269        if (!mChildrenExpanded) {
270            intrinsicHeight += mCollapsedBottompadding;
271        }
272        return intrinsicHeight;
273    }
274
275    /**
276     * Update the state of all its children based on a linear layout algorithm.
277     *
278     * @param resultState the state to update
279     * @param parentState the state of the parent
280     */
281    public void getState(StackScrollState resultState, StackViewState parentState) {
282        int childCount = mChildren.size();
283        int yPosition = mNotificationHeaderHeight;
284        boolean firstChild = true;
285        int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
286        boolean hasOverflow = !mChildrenExpanded && childCount > maxAllowedVisibleChildren
287                && maxAllowedVisibleChildren != NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
288        int lastVisibleIndex = hasOverflow
289                ? maxAllowedVisibleChildren - 2
290                : maxAllowedVisibleChildren - 1;
291        for (int i = 0; i < childCount; i++) {
292            ExpandableNotificationRow child = mChildren.get(i);
293            if (!firstChild) {
294                yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding;
295            } else {
296                yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0;
297                firstChild = false;
298            }
299            StackViewState childState = resultState.getViewStateForView(child);
300            int intrinsicHeight = child.getIntrinsicHeight();
301            childState.yTranslation = yPosition;
302            childState.zTranslation = 0;
303            childState.height = intrinsicHeight;
304            childState.dimmed = parentState.dimmed;
305            childState.dark = parentState.dark;
306            childState.hideSensitive = parentState.hideSensitive;
307            childState.belowSpeedBump = parentState.belowSpeedBump;
308            childState.scale =  1.0f;
309            childState.clipTopAmount = 0;
310            childState.topOverLap = 0;
311            boolean visible = i <= lastVisibleIndex;
312            childState.alpha = visible ? 1 : 0;
313            childState.location = parentState.location;
314            yPosition += intrinsicHeight;
315        }
316        if (mGroupOverflowContainer != null) {
317            mGroupOverFlowState.initFrom(mGroupOverflowContainer);
318            if (hasOverflow) {
319                StackViewState firstOverflowState =
320                        resultState.getViewStateForView(mChildren.get(lastVisibleIndex + 1));
321                mGroupOverFlowState.yTranslation = firstOverflowState.yTranslation;
322            }
323            mGroupOverFlowState.alpha = mChildrenExpanded || !hasOverflow ? 0.0f : 1.0f;
324        }
325    }
326
327    private int getMaxAllowedVisibleChildren() {
328        return getMaxAllowedVisibleChildren(false /* likeCollapsed */);
329    }
330
331    private int getMaxAllowedVisibleChildren(boolean likeCollapsed) {
332        if (!likeCollapsed && (mChildrenExpanded || mNotificationParent.isUserLocked())) {
333            return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
334        }
335        if (mNotificationParent.isExpanded()) {
336            return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
337        }
338        return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
339    }
340
341    public void applyState(StackScrollState state) {
342        int childCount = mChildren.size();
343        ViewState tmpState = new ViewState();
344        for (int i = 0; i < childCount; i++) {
345            ExpandableNotificationRow child = mChildren.get(i);
346            StackViewState viewState = state.getViewStateForView(child);
347            state.applyState(child, viewState);
348
349            // layout the divider
350            View divider = mDividers.get(i);
351            tmpState.initFrom(divider);
352            tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
353            tmpState.alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
354            state.applyViewState(divider, tmpState);
355        }
356        if (mGroupOverflowContainer != null) {
357            state.applyViewState(mGroupOverflowContainer, mGroupOverFlowState);
358        }
359    }
360
361    /**
362     * This is called when the children expansion has changed and positions the children properly
363     * for an appear animation.
364     *
365     * @param state the new state we animate to
366     */
367    public void prepareExpansionChanged(StackScrollState state) {
368        if (true) {
369            // TODO: do something that makes sense
370            return;
371        }
372        int childCount = mChildren.size();
373        StackViewState sourceState = new StackViewState();
374        ViewState dividerState = new ViewState();
375        for (int i = 0; i < childCount; i++) {
376            ExpandableNotificationRow child = mChildren.get(i);
377            StackViewState viewState = state.getViewStateForView(child);
378            sourceState.copyFrom(viewState);
379            sourceState.alpha = 0;
380            sourceState.yTranslation += mNotificationAppearDistance;
381            state.applyState(child, sourceState);
382
383            // layout the divider
384            View divider = mDividers.get(i);
385            dividerState.initFrom(divider);
386            dividerState.yTranslation = viewState.yTranslation - mDividerHeight
387                    + mNotificationAppearDistance;
388            dividerState.alpha = 0;
389            state.applyViewState(divider, dividerState);
390
391        }
392    }
393
394    public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator,
395            boolean withDelays, long baseDelay, long duration) {
396        int childCount = mChildren.size();
397        ViewState tmpState = new ViewState();
398        int delayIndex = 0;
399        int maxAllowChildCount = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
400        for (int i = childCount - 1; i >= 0; i--) {
401            ExpandableNotificationRow child = mChildren.get(i);
402            StackViewState viewState = state.getViewStateForView(child);
403            int difference = Math.min(StackStateAnimator.DELAY_EFFECT_MAX_INDEX_DIFFERENCE_CHILDREN,
404                    delayIndex);
405            long delay = withDelays
406                    ? difference * StackStateAnimator.ANIMATION_DELAY_PER_ELEMENT_EXPAND_CHILDREN
407                    : 0;
408            delay = (long) (delay * (mChildrenExpanded ? 1.0f : 0.5f) + baseDelay);
409            stateAnimator.startStackAnimations(child, viewState, state, -1, delay);
410
411            // layout the divider
412            View divider = mDividers.get(i);
413            tmpState.initFrom(divider);
414            tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
415            tmpState.alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
416            stateAnimator.startViewAnimations(divider, tmpState, delay, duration);
417            if (i < maxAllowChildCount) {
418                delayIndex++;
419            }
420        }
421        if (mGroupOverflowContainer != null) {
422            stateAnimator.startViewAnimations(mGroupOverflowContainer, mGroupOverFlowState,
423                    baseDelay, duration);
424        }
425    }
426
427    public ExpandableNotificationRow getViewAtPosition(float y) {
428        // find the view under the pointer, accounting for GONE views
429        final int count = mChildren.size();
430        for (int childIdx = 0; childIdx < count; childIdx++) {
431            ExpandableNotificationRow slidingChild = mChildren.get(childIdx);
432            float childTop = slidingChild.getTranslationY();
433            float top = childTop + slidingChild.getClipTopAmount();
434            float bottom = childTop + slidingChild.getActualHeight();
435            if (y >= top && y <= bottom) {
436                return slidingChild;
437            }
438        }
439        return null;
440    }
441
442    public void setChildrenExpanded(boolean childrenExpanded) {
443        mChildrenExpanded = childrenExpanded;
444    }
445
446    public void setNotificationParent(ExpandableNotificationRow parent) {
447        mNotificationParent = parent;
448    }
449
450    public int getMaxContentHeight() {
451        return getIntrinsicHeight(NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
452    }
453
454    public int getMinHeight() {
455        return getIntrinsicHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */));
456    }
457
458    public void setDark(boolean dark, boolean fade, long delay) {
459        if (mGroupOverflowContainer != null) {
460            mOverflowInvertHelper.setInverted(dark, fade, delay);
461        }
462    }
463}
464