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