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