2 * Copyright (C) 2015 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.systemui.statusbar.notification.stack;
19 import android.app.Notification;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.content.res.Resources;
23 import android.graphics.drawable.ColorDrawable;
24 import android.service.notification.StatusBarNotification;
25 import android.util.AttributeSet;
26 import android.view.LayoutInflater;
27 import android.view.NotificationHeaderView;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.widget.RemoteViews;
31 import android.widget.TextView;
33 import com.android.internal.annotations.VisibleForTesting;
34 import com.android.systemui.R;
35 import com.android.systemui.statusbar.CrossFadeHelper;
36 import com.android.systemui.statusbar.NotificationHeaderUtil;
37 import com.android.systemui.statusbar.notification.NotificationUtils;
38 import com.android.systemui.statusbar.notification.VisualStabilityManager;
39 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
40 import com.android.systemui.statusbar.notification.row.HybridGroupManager;
41 import com.android.systemui.statusbar.notification.row.HybridNotificationView;
42 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
44 import java.util.ArrayList;
45 import java.util.List;
48 * A container containing child notifications
50 public class NotificationChildrenContainer extends ViewGroup {
53 static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2;
55 static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5;
57 static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8;
58 private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() {
59 private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
62 public AnimationFilter getAnimationFilter() {
63 return mAnimationFilter;
67 private final List<View> mDividers = new ArrayList<>();
68 private final List<ExpandableNotificationRow> mChildren = new ArrayList<>();
69 private final HybridGroupManager mHybridGroupManager;
70 private int mChildPadding;
71 private int mDividerHeight;
72 private float mDividerAlpha;
73 private int mNotificationHeaderMargin;
75 private int mNotificatonTopPadding;
76 private float mCollapsedBottompadding;
77 private boolean mChildrenExpanded;
78 private ExpandableNotificationRow mContainingNotification;
79 private TextView mOverflowNumber;
80 private ViewState mGroupOverFlowState;
81 private int mRealHeight;
82 private boolean mUserLocked;
83 private int mActualHeight;
84 private boolean mNeverAppliedGroupState;
85 private int mHeaderHeight;
88 * Whether or not individual notifications that are part of this container will have shadows.
90 private boolean mEnableShadowOnChildNotifications;
92 private NotificationHeaderView mNotificationHeader;
93 private NotificationViewWrapper mNotificationHeaderWrapper;
94 private NotificationHeaderView mNotificationHeaderLowPriority;
95 private NotificationViewWrapper mNotificationHeaderWrapperLowPriority;
96 private NotificationHeaderUtil mHeaderUtil;
97 private ViewState mHeaderViewState;
98 private int mClipBottomAmount;
99 private boolean mIsLowPriority;
100 private OnClickListener mHeaderClickListener;
101 private ViewGroup mCurrentHeader;
103 private boolean mShowDividersWhenExpanded;
104 private boolean mHideDividersDuringExpand;
105 private int mTranslationForHeader;
106 private int mCurrentHeaderTranslation = 0;
107 private float mHeaderVisibleAmount = 1.0f;
109 public NotificationChildrenContainer(Context context) {
113 public NotificationChildrenContainer(Context context, AttributeSet attrs) {
114 this(context, attrs, 0);
117 public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) {
118 this(context, attrs, defStyleAttr, 0);
121 public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr,
123 super(context, attrs, defStyleAttr, defStyleRes);
124 mHybridGroupManager = new HybridGroupManager(getContext(), this);
126 setClipChildren(false);
129 private void initDimens() {
130 Resources res = getResources();
131 mChildPadding = res.getDimensionPixelSize(R.dimen.notification_children_padding);
132 mDividerHeight = res.getDimensionPixelSize(
133 R.dimen.notification_children_container_divider_height);
134 mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha);
135 mNotificationHeaderMargin = res.getDimensionPixelSize(
136 R.dimen.notification_children_container_margin_top);
137 mNotificatonTopPadding = res.getDimensionPixelSize(
138 R.dimen.notification_children_container_top_padding);
139 mHeaderHeight = mNotificationHeaderMargin + mNotificatonTopPadding;
140 mCollapsedBottompadding = res.getDimensionPixelSize(
141 com.android.internal.R.dimen.notification_content_margin);
142 mEnableShadowOnChildNotifications =
143 res.getBoolean(R.bool.config_enableShadowOnChildNotifications);
144 mShowDividersWhenExpanded =
145 res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded);
146 mHideDividersDuringExpand =
147 res.getBoolean(R.bool.config_hideDividersDuringExpand);
148 mTranslationForHeader = res.getDimensionPixelSize(
149 com.android.internal.R.dimen.notification_content_margin)
150 - mNotificationHeaderMargin;
151 mHybridGroupManager.initDimens();
155 protected void onLayout(boolean changed, int l, int t, int r, int b) {
156 int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
157 for (int i = 0; i < childCount; i++) {
158 View child = mChildren.get(i);
159 // We need to layout all children even the GONE ones, such that the heights are
160 // calculated correctly as they are used to calculate how many we can fit on the screen
161 child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
162 mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
164 if (mOverflowNumber != null) {
165 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
166 int left = (isRtl ? 0 : getWidth() - mOverflowNumber.getMeasuredWidth());
167 int right = left + mOverflowNumber.getMeasuredWidth();
168 mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight());
170 if (mNotificationHeader != null) {
171 mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(),
172 mNotificationHeader.getMeasuredHeight());
174 if (mNotificationHeaderLowPriority != null) {
175 mNotificationHeaderLowPriority.layout(0, 0,
176 mNotificationHeaderLowPriority.getMeasuredWidth(),
177 mNotificationHeaderLowPriority.getMeasuredHeight());
182 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
183 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
184 boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
185 boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
186 int size = MeasureSpec.getSize(heightMeasureSpec);
187 int newHeightSpec = heightMeasureSpec;
188 if (hasFixedHeight || isHeightLimited) {
189 newHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
191 int width = MeasureSpec.getSize(widthMeasureSpec);
192 if (mOverflowNumber != null) {
193 mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
196 int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY);
197 int height = mNotificationHeaderMargin + mNotificatonTopPadding;
198 int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
199 int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
200 int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1;
201 for (int i = 0; i < childCount; i++) {
202 ExpandableNotificationRow child = mChildren.get(i);
203 // We need to measure all children even the GONE ones, such that the heights are
204 // calculated correctly as they are used to calculate how many we can fit on the screen.
205 boolean isOverflow = i == overflowIndex;
206 child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null
207 ? mOverflowNumber.getMeasuredWidth() : 0);
208 child.measure(widthMeasureSpec, newHeightSpec);
209 // layout the divider
210 View divider = mDividers.get(i);
211 divider.measure(widthMeasureSpec, dividerHeightSpec);
212 if (child.getVisibility() != GONE) {
213 height += child.getMeasuredHeight() + mDividerHeight;
216 mRealHeight = height;
217 if (heightMode != MeasureSpec.UNSPECIFIED) {
218 height = Math.min(height, size);
221 int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
222 if (mNotificationHeader != null) {
223 mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec);
225 if (mNotificationHeaderLowPriority != null) {
226 headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY);
227 mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec);
230 setMeasuredDimension(width, height);
234 public boolean hasOverlappingRendering() {
239 public boolean pointInView(float localX, float localY, float slop) {
240 return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
241 localY < (mRealHeight + slop);
245 * Add a child notification to this view.
247 * @param row the row to add
248 * @param childIndex the index to add it at, if -1 it will be added at the end
250 public void addNotification(ExpandableNotificationRow row, int childIndex) {
251 int newIndex = childIndex < 0 ? mChildren.size() : childIndex;
252 mChildren.add(newIndex, row);
254 row.setUserLocked(mUserLocked);
256 View divider = inflateDivider();
258 mDividers.add(newIndex, divider);
260 updateGroupOverflow();
261 row.setContentTransformationAmount(0, false /* isLastChild */);
262 // It doesn't make sense to keep old animations around, lets cancel them!
263 ExpandableViewState viewState = row.getViewState();
264 if (viewState != null) {
265 viewState.cancelAnimations(row);
266 row.cancelAppearDrawing();
270 public void removeNotification(ExpandableNotificationRow row) {
271 int childIndex = mChildren.indexOf(row);
272 mChildren.remove(row);
275 final View divider = mDividers.remove(childIndex);
277 getOverlay().add(divider);
278 CrossFadeHelper.fadeOut(divider, new Runnable() {
281 getOverlay().remove(divider);
285 row.setSystemChildExpanded(false);
286 row.setUserLocked(false);
287 updateGroupOverflow();
288 if (!row.isRemoved()) {
289 mHeaderUtil.restoreNotificationHeader(row);
294 * @return The number of notification children in the container.
296 public int getNotificationChildCount() {
297 return mChildren.size();
300 public void recreateNotificationHeader(OnClickListener listener) {
301 mHeaderClickListener = listener;
302 StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
303 final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(),
304 notification.getNotification());
305 RemoteViews header = builder.makeNotificationHeader();
306 if (mNotificationHeader == null) {
307 mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
308 final View expandButton = mNotificationHeader.findViewById(
309 com.android.internal.R.id.expand_button);
310 expandButton.setVisibility(VISIBLE);
311 mNotificationHeader.setOnClickListener(mHeaderClickListener);
312 mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(),
313 mNotificationHeader, mContainingNotification);
314 addView(mNotificationHeader, 0);
317 header.reapply(getContext(), mNotificationHeader);
319 mNotificationHeaderWrapper.onContentUpdated(mContainingNotification);
320 recreateLowPriorityHeader(builder);
321 updateHeaderVisibility(false /* animate */);
322 updateChildrenHeaderAppearance();
326 * Recreate the low-priority header.
328 * @param builder a builder to reuse. Otherwise the builder will be recovered.
330 private void recreateLowPriorityHeader(Notification.Builder builder) {
332 StatusBarNotification notification = mContainingNotification.getStatusBarNotification();
333 if (mIsLowPriority) {
334 if (builder == null) {
335 builder = Notification.Builder.recoverBuilder(getContext(),
336 notification.getNotification());
338 header = builder.makeLowPriorityContentView(true /* useRegularSubtext */);
339 if (mNotificationHeaderLowPriority == null) {
340 mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(),
342 final View expandButton = mNotificationHeaderLowPriority.findViewById(
343 com.android.internal.R.id.expand_button);
344 expandButton.setVisibility(VISIBLE);
345 mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
346 mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(),
347 mNotificationHeaderLowPriority, mContainingNotification);
348 addView(mNotificationHeaderLowPriority, 0);
351 header.reapply(getContext(), mNotificationHeaderLowPriority);
353 mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification);
354 resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader());
356 removeView(mNotificationHeaderLowPriority);
357 mNotificationHeaderLowPriority = null;
358 mNotificationHeaderWrapperLowPriority = null;
362 public void updateChildrenHeaderAppearance() {
363 mHeaderUtil.updateChildrenHeaderAppearance();
366 public void updateGroupOverflow() {
367 int childCount = mChildren.size();
368 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
369 if (childCount > maxAllowedVisibleChildren) {
370 int number = childCount - maxAllowedVisibleChildren;
371 mOverflowNumber = mHybridGroupManager.bindOverflowNumber(mOverflowNumber, number);
372 if (mGroupOverFlowState == null) {
373 mGroupOverFlowState = new ViewState();
374 mNeverAppliedGroupState = true;
376 } else if (mOverflowNumber != null) {
377 removeView(mOverflowNumber);
378 if (isShown() && isAttachedToWindow()) {
379 final View removedOverflowNumber = mOverflowNumber;
380 addTransientView(removedOverflowNumber, getTransientViewCount());
381 CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() {
384 removeTransientView(removedOverflowNumber);
388 mOverflowNumber = null;
389 mGroupOverFlowState = null;
394 protected void onConfigurationChanged(Configuration newConfig) {
395 super.onConfigurationChanged(newConfig);
396 updateGroupOverflow();
399 private View inflateDivider() {
400 return LayoutInflater.from(mContext).inflate(
401 R.layout.notification_children_divider, this, false);
404 public List<ExpandableNotificationRow> getNotificationChildren() {
409 * Apply the order given in the list to the children.
411 * @param childOrder the new list order
412 * @param visualStabilityManager
414 * @return whether the list order has changed
416 public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder,
417 VisualStabilityManager visualStabilityManager,
418 VisualStabilityManager.Callback callback) {
419 if (childOrder == null) {
422 boolean result = false;
423 for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) {
424 ExpandableNotificationRow child = mChildren.get(i);
425 ExpandableNotificationRow desiredChild = childOrder.get(i);
426 if (child != desiredChild) {
427 if (visualStabilityManager.canReorderNotification(desiredChild)) {
428 mChildren.remove(desiredChild);
429 mChildren.add(i, desiredChild);
432 visualStabilityManager.addReorderingAllowedCallback(callback);
436 updateExpansionStates();
440 private void updateExpansionStates() {
441 if (mChildrenExpanded || mUserLocked) {
442 // we don't modify it the group is expanded or if we are expanding it
445 int size = mChildren.size();
446 for (int i = 0; i < size; i++) {
447 ExpandableNotificationRow child = mChildren.get(i);
448 child.setSystemChildExpanded(i == 0 && size == 1);
454 * @return the intrinsic size of this children container, i.e the natural fully expanded state
456 public int getIntrinsicHeight() {
457 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
458 return getIntrinsicHeight(maxAllowedVisibleChildren);
462 * @return the intrinsic height with a number of children given
463 * in @param maxAllowedVisibleChildren
465 private int getIntrinsicHeight(float maxAllowedVisibleChildren) {
466 if (showingAsLowPriority()) {
467 return mNotificationHeaderLowPriority.getHeight();
469 int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation;
470 int visibleChildren = 0;
471 int childCount = mChildren.size();
472 boolean firstChild = true;
473 float expandFactor = 0;
475 expandFactor = getGroupExpandFraction();
477 boolean childrenExpanded = mChildrenExpanded;
478 for (int i = 0; i < childCount; i++) {
479 if (visibleChildren >= maxAllowedVisibleChildren) {
484 intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
487 intrinsicHeight += childrenExpanded ? mDividerHeight : mChildPadding;
491 intrinsicHeight += NotificationUtils.interpolate(
493 mNotificatonTopPadding + mDividerHeight,
496 intrinsicHeight += childrenExpanded
497 ? mNotificatonTopPadding + mDividerHeight
502 ExpandableNotificationRow child = mChildren.get(i);
503 intrinsicHeight += child.getIntrinsicHeight();
507 intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottompadding, 0.0f,
509 } else if (!childrenExpanded) {
510 intrinsicHeight += mCollapsedBottompadding;
512 return intrinsicHeight;
516 * Update the state of all its children based on a linear layout algorithm.
517 * @param parentState the state of the parent
518 * @param ambientState the ambient state containing ambient information
520 public void updateState(ExpandableViewState parentState, AmbientState ambientState) {
521 int childCount = mChildren.size();
522 int yPosition = mNotificationHeaderMargin + mCurrentHeaderTranslation;
523 boolean firstChild = true;
524 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
525 int lastVisibleIndex = maxAllowedVisibleChildren - 1;
526 int firstOverflowIndex = lastVisibleIndex + 1;
527 float expandFactor = 0;
528 boolean expandingToExpandedGroup = mUserLocked && !showingAsLowPriority();
530 expandFactor = getGroupExpandFraction();
531 firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
534 boolean childrenExpandedAndNotAnimating = mChildrenExpanded
535 && !mContainingNotification.isGroupExpansionChanging();
536 int launchTransitionCompensation = 0;
537 for (int i = 0; i < childCount; i++) {
538 ExpandableNotificationRow child = mChildren.get(i);
540 if (expandingToExpandedGroup) {
541 yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
544 yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding;
547 if (expandingToExpandedGroup) {
548 yPosition += NotificationUtils.interpolate(
550 mNotificatonTopPadding + mDividerHeight,
553 yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0;
558 ExpandableViewState childState = child.getViewState();
559 int intrinsicHeight = child.getIntrinsicHeight();
560 childState.height = intrinsicHeight;
561 childState.yTranslation = yPosition + launchTransitionCompensation;
562 childState.hidden = false;
563 // When the group is expanded, the children cast the shadows rather than the parent
564 // so use the parent's elevation here.
565 childState.zTranslation =
566 (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications)
567 ? parentState.zTranslation
569 childState.dimmed = parentState.dimmed;
570 childState.hideSensitive = parentState.hideSensitive;
571 childState.belowSpeedBump = parentState.belowSpeedBump;
572 childState.clipTopAmount = 0;
573 childState.alpha = 0;
574 if (i < firstOverflowIndex) {
575 childState.alpha = showingAsLowPriority() ? expandFactor : 1.0f;
576 } else if (expandFactor == 1.0f && i <= lastVisibleIndex) {
577 childState.alpha = (mActualHeight - childState.yTranslation) / childState.height;
578 childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha));
580 childState.location = parentState.location;
581 childState.inShelf = parentState.inShelf;
582 yPosition += intrinsicHeight;
583 if (child.isExpandAnimationRunning()) {
584 launchTransitionCompensation = -ambientState.getExpandAnimationTopChange();
588 if (mOverflowNumber != null) {
589 ExpandableNotificationRow overflowView = mChildren.get(Math.min(
590 getMaxAllowedVisibleChildren(true /* likeCollapsed */), childCount) - 1);
591 mGroupOverFlowState.copyFrom(overflowView.getViewState());
593 if (!mChildrenExpanded) {
594 HybridNotificationView alignView = overflowView.getSingleLineView();
595 if (alignView != null) {
596 View mirrorView = alignView.getTextView();
597 if (mirrorView.getVisibility() == GONE) {
598 mirrorView = alignView.getTitleView();
600 if (mirrorView.getVisibility() == GONE) {
601 mirrorView = alignView;
603 mGroupOverFlowState.alpha = mirrorView.getAlpha();
604 mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset(
605 mirrorView, overflowView);
608 mGroupOverFlowState.yTranslation += mNotificationHeaderMargin;
609 mGroupOverFlowState.alpha = 0.0f;
612 if (mNotificationHeader != null) {
613 if (mHeaderViewState == null) {
614 mHeaderViewState = new ViewState();
616 mHeaderViewState.initFrom(mNotificationHeader);
617 mHeaderViewState.zTranslation = childrenExpandedAndNotAnimating
618 ? parentState.zTranslation
620 mHeaderViewState.yTranslation = mCurrentHeaderTranslation;
621 mHeaderViewState.alpha = mHeaderVisibleAmount;
622 // The hiding is done automatically by the alpha, otherwise we'll pick it up again
623 // in the next frame with the initFrom call above and have an invisible header
624 mHeaderViewState.hidden = false;
629 * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its
630 * height, children in the group after this are gone.
632 * @param child the child who's height to adjust.
633 * @param parentHeight the height of the parent.
634 * @param childState the state to update.
635 * @param yPosition the yPosition of the view.
636 * @return true if children after this one should be hidden.
638 private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child,
639 int parentHeight, ExpandableViewState childState, int yPosition) {
640 final int top = yPosition + child.getClipTopAmount();
641 final int intrinsicHeight = child.getIntrinsicHeight();
642 final int bottom = top + intrinsicHeight;
643 int newHeight = intrinsicHeight;
644 if (bottom >= parentHeight) {
645 // Child is either clipped or gone
646 newHeight = Math.max((parentHeight - top), 0);
648 childState.hidden = newHeight == 0;
649 childState.height = newHeight;
650 return childState.height != intrinsicHeight && !childState.hidden;
654 int getMaxAllowedVisibleChildren() {
655 return getMaxAllowedVisibleChildren(false /* likeCollapsed */);
659 int getMaxAllowedVisibleChildren(boolean likeCollapsed) {
660 if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked())
661 && !showingAsLowPriority()) {
662 return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
665 || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded())
666 || (mContainingNotification.isHeadsUpState()
667 && mContainingNotification.canShowHeadsUp())) {
668 return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
670 return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
673 /** Applies state to children. */
674 public void applyState() {
675 int childCount = mChildren.size();
676 ViewState tmpState = new ViewState();
677 float expandFraction = 0.0f;
679 expandFraction = getGroupExpandFraction();
681 final boolean dividersVisible = mUserLocked && !showingAsLowPriority()
682 || (mChildrenExpanded && mShowDividersWhenExpanded)
683 || (mContainingNotification.isGroupExpansionChanging()
684 && !mHideDividersDuringExpand);
685 for (int i = 0; i < childCount; i++) {
686 ExpandableNotificationRow child = mChildren.get(i);
687 ExpandableViewState viewState = child.getViewState();
688 viewState.applyToView(child);
690 // layout the divider
691 View divider = mDividers.get(i);
692 tmpState.initFrom(divider);
693 tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
694 float alpha = mChildrenExpanded && viewState.alpha != 0 ? mDividerAlpha : 0;
695 if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) {
696 alpha = NotificationUtils.interpolate(0, 0.5f,
697 Math.min(viewState.alpha, expandFraction));
699 tmpState.hidden = !dividersVisible;
700 tmpState.alpha = alpha;
701 tmpState.applyToView(divider);
702 // There is no fake shadow to be drawn on the children
703 child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
705 if (mGroupOverFlowState != null) {
706 mGroupOverFlowState.applyToView(mOverflowNumber);
707 mNeverAppliedGroupState = false;
709 if (mHeaderViewState != null) {
710 mHeaderViewState.applyToView(mNotificationHeader);
712 updateChildrenClipping();
715 private void updateChildrenClipping() {
716 if (mContainingNotification.hasExpandingChild()) {
719 int childCount = mChildren.size();
720 int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount;
721 for (int i = 0; i < childCount; i++) {
722 ExpandableNotificationRow child = mChildren.get(i);
723 if (child.getVisibility() == GONE) {
726 float childTop = child.getTranslationY();
727 float childBottom = childTop + child.getActualHeight();
728 boolean visible = true;
729 int clipBottomAmount = 0;
730 if (childTop > layoutEnd) {
732 } else if (childBottom > layoutEnd) {
733 clipBottomAmount = (int) (childBottom - layoutEnd);
736 boolean isVisible = child.getVisibility() == VISIBLE;
737 if (visible != isVisible) {
738 child.setVisibility(visible ? VISIBLE : INVISIBLE);
741 child.setClipBottomAmount(clipBottomAmount);
746 * This is called when the children expansion has changed and positions the children properly
747 * for an appear animation.
750 public void prepareExpansionChanged() {
751 // TODO: do something that makes sense, like placing the invisible views correctly
755 /** Animate to a given state. */
756 public void startAnimationToState(AnimationProperties properties) {
757 int childCount = mChildren.size();
758 ViewState tmpState = new ViewState();
759 float expandFraction = getGroupExpandFraction();
760 final boolean dividersVisible = mUserLocked && !showingAsLowPriority()
761 || (mChildrenExpanded && mShowDividersWhenExpanded)
762 || (mContainingNotification.isGroupExpansionChanging()
763 && !mHideDividersDuringExpand);
764 for (int i = childCount - 1; i >= 0; i--) {
765 ExpandableNotificationRow child = mChildren.get(i);
766 ExpandableViewState viewState = child.getViewState();
767 viewState.animateTo(child, properties);
769 // layout the divider
770 View divider = mDividers.get(i);
771 tmpState.initFrom(divider);
772 tmpState.yTranslation = viewState.yTranslation - mDividerHeight;
773 float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0;
774 if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) {
775 alpha = NotificationUtils.interpolate(0, 0.5f,
776 Math.min(viewState.alpha, expandFraction));
778 tmpState.hidden = !dividersVisible;
779 tmpState.alpha = alpha;
780 tmpState.animateTo(divider, properties);
781 // There is no fake shadow to be drawn on the children
782 child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0);
784 if (mOverflowNumber != null) {
785 if (mNeverAppliedGroupState) {
786 float alpha = mGroupOverFlowState.alpha;
787 mGroupOverFlowState.alpha = 0;
788 mGroupOverFlowState.applyToView(mOverflowNumber);
789 mGroupOverFlowState.alpha = alpha;
790 mNeverAppliedGroupState = false;
792 mGroupOverFlowState.animateTo(mOverflowNumber, properties);
794 if (mNotificationHeader != null) {
795 mHeaderViewState.applyToView(mNotificationHeader);
797 updateChildrenClipping();
800 public ExpandableNotificationRow getViewAtPosition(float y) {
801 // find the view under the pointer, accounting for GONE views
802 final int count = mChildren.size();
803 for (int childIdx = 0; childIdx < count; childIdx++) {
804 ExpandableNotificationRow slidingChild = mChildren.get(childIdx);
805 float childTop = slidingChild.getTranslationY();
806 float top = childTop + slidingChild.getClipTopAmount();
807 float bottom = childTop + slidingChild.getActualHeight();
808 if (y >= top && y <= bottom) {
815 public void setChildrenExpanded(boolean childrenExpanded) {
816 mChildrenExpanded = childrenExpanded;
817 updateExpansionStates();
818 if (mNotificationHeader != null) {
819 mNotificationHeader.setExpanded(childrenExpanded);
821 final int count = mChildren.size();
822 for (int childIdx = 0; childIdx < count; childIdx++) {
823 ExpandableNotificationRow child = mChildren.get(childIdx);
824 child.setChildrenExpanded(childrenExpanded, false);
826 updateHeaderTouchability();
829 public void setContainingNotification(ExpandableNotificationRow parent) {
830 mContainingNotification = parent;
831 mHeaderUtil = new NotificationHeaderUtil(mContainingNotification);
834 public ExpandableNotificationRow getContainingNotification() {
835 return mContainingNotification;
838 public NotificationHeaderView getHeaderView() {
839 return mNotificationHeader;
842 public NotificationHeaderView getLowPriorityHeaderView() {
843 return mNotificationHeaderLowPriority;
847 public ViewGroup getCurrentHeaderView() {
848 return mCurrentHeader;
851 private void updateHeaderVisibility(boolean animate) {
852 ViewGroup desiredHeader;
853 ViewGroup currentHeader = mCurrentHeader;
854 desiredHeader = calculateDesiredHeader();
856 if (currentHeader == desiredHeader) {
861 if (desiredHeader != null && currentHeader != null) {
862 currentHeader.setVisibility(VISIBLE);
863 desiredHeader.setVisibility(VISIBLE);
864 NotificationViewWrapper visibleWrapper = getWrapperForView(desiredHeader);
865 NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader);
866 visibleWrapper.transformFrom(hiddenWrapper);
867 hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false));
868 startChildAlphaAnimations(desiredHeader == mNotificationHeader);
874 if (desiredHeader != null) {
875 getWrapperForView(desiredHeader).setVisible(true);
876 desiredHeader.setVisibility(VISIBLE);
878 if (currentHeader != null) {
879 // Wrapper can be null if we were a low priority notification
880 // and just destroyed it by calling setIsLowPriority(false)
881 NotificationViewWrapper wrapper = getWrapperForView(currentHeader);
882 if (wrapper != null) {
883 wrapper.setVisible(false);
885 currentHeader.setVisibility(INVISIBLE);
889 resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader);
890 resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader);
892 mCurrentHeader = desiredHeader;
895 private void resetHeaderVisibilityIfNeeded(View header, View desiredHeader) {
896 if (header == null) {
899 if (header != mCurrentHeader && header != desiredHeader) {
900 getWrapperForView(header).setVisible(false);
901 header.setVisibility(INVISIBLE);
903 if (header == desiredHeader && header.getVisibility() != VISIBLE) {
904 getWrapperForView(header).setVisible(true);
905 header.setVisibility(VISIBLE);
909 private ViewGroup calculateDesiredHeader() {
910 ViewGroup desiredHeader;
911 if (showingAsLowPriority()) {
912 desiredHeader = mNotificationHeaderLowPriority;
914 desiredHeader = mNotificationHeader;
916 return desiredHeader;
919 private void startChildAlphaAnimations(boolean toVisible) {
920 float target = toVisible ? 1.0f : 0.0f;
921 float start = 1.0f - target;
922 int childCount = mChildren.size();
923 for (int i = 0; i < childCount; i++) {
924 if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) {
927 ExpandableNotificationRow child = mChildren.get(i);
928 child.setAlpha(start);
929 ViewState viewState = new ViewState();
930 viewState.initFrom(child);
931 viewState.alpha = target;
932 ALPHA_FADE_IN.setDelay(i * 50);
933 viewState.animateTo(child, ALPHA_FADE_IN);
938 private void updateHeaderTransformation() {
939 if (mUserLocked && showingAsLowPriority()) {
940 float fraction = getGroupExpandFraction();
941 mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority,
943 mNotificationHeader.setVisibility(VISIBLE);
944 mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper,
950 private NotificationViewWrapper getWrapperForView(View visibleHeader) {
951 if (visibleHeader == mNotificationHeader) {
952 return mNotificationHeaderWrapper;
954 return mNotificationHeaderWrapperLowPriority;
958 * Called when a groups expansion changes to adjust the background of the header view.
960 * @param expanded whether the group is expanded.
962 public void updateHeaderForExpansion(boolean expanded) {
963 if (mNotificationHeader != null) {
965 ColorDrawable cd = new ColorDrawable();
966 cd.setColor(mContainingNotification.calculateBgColor());
967 mNotificationHeader.setHeaderBackgroundDrawable(cd);
969 mNotificationHeader.setHeaderBackgroundDrawable(null);
974 public int getMaxContentHeight() {
975 if (showingAsLowPriority()) {
976 return getMinHeight(NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED, true
977 /* likeHighPriority */);
979 int maxContentHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation
980 + mNotificatonTopPadding;
981 int visibleChildren = 0;
982 int childCount = mChildren.size();
983 for (int i = 0; i < childCount; i++) {
984 if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) {
987 ExpandableNotificationRow child = mChildren.get(i);
988 float childHeight = child.isExpanded(true /* allowOnKeyguard */)
989 ? child.getMaxExpandHeight()
990 : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
991 maxContentHeight += childHeight;
994 if (visibleChildren > 0) {
995 maxContentHeight += visibleChildren * mDividerHeight;
997 return maxContentHeight;
1000 public void setActualHeight(int actualHeight) {
1004 mActualHeight = actualHeight;
1005 float fraction = getGroupExpandFraction();
1006 boolean showingLowPriority = showingAsLowPriority();
1007 updateHeaderTransformation();
1008 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
1009 int childCount = mChildren.size();
1010 for (int i = 0; i < childCount; i++) {
1011 ExpandableNotificationRow child = mChildren.get(i);
1013 if (showingLowPriority) {
1014 childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */);
1015 } else if (child.isExpanded(true /* allowOnKeyguard */)) {
1016 childHeight = child.getMaxExpandHeight();
1018 childHeight = child.getShowingLayout().getMinHeight(
1019 true /* likeGroupExpanded */);
1021 if (i < maxAllowedVisibleChildren) {
1022 float singleLineHeight = child.getShowingLayout().getMinHeight(
1023 false /* likeGroupExpanded */);
1024 child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight,
1025 childHeight, fraction), false);
1027 child.setActualHeight((int) childHeight, false);
1032 public float getGroupExpandFraction() {
1033 int visibleChildrenExpandedHeight = showingAsLowPriority() ? getMaxContentHeight()
1034 : getVisibleChildrenExpandHeight();
1035 int minExpandHeight = getCollapsedHeight();
1036 float factor = (mActualHeight - minExpandHeight)
1037 / (float) (visibleChildrenExpandedHeight - minExpandHeight);
1038 return Math.max(0.0f, Math.min(1.0f, factor));
1041 private int getVisibleChildrenExpandHeight() {
1042 int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation
1043 + mNotificatonTopPadding + mDividerHeight;
1044 int visibleChildren = 0;
1045 int childCount = mChildren.size();
1046 int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
1047 for (int i = 0; i < childCount; i++) {
1048 if (visibleChildren >= maxAllowedVisibleChildren) {
1051 ExpandableNotificationRow child = mChildren.get(i);
1052 float childHeight = child.isExpanded(true /* allowOnKeyguard */)
1053 ? child.getMaxExpandHeight()
1054 : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */);
1055 intrinsicHeight += childHeight;
1058 return intrinsicHeight;
1061 public int getMinHeight() {
1062 return getMinHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED, false /* likeHighPriority */);
1065 public int getCollapsedHeight() {
1066 return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */),
1067 false /* likeHighPriority */);
1070 public int getCollapsedHeightWithoutHeader() {
1071 return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */),
1072 false /* likeHighPriority */, 0);
1076 * Get the minimum Height for this group.
1078 * @param maxAllowedVisibleChildren the number of children that should be visible
1079 * @param likeHighPriority if the height should be calculated as if it were not low priority
1081 private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) {
1082 return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation);
1086 * Get the minimum Height for this group.
1088 * @param maxAllowedVisibleChildren the number of children that should be visible
1089 * @param likeHighPriority if the height should be calculated as if it were not low priority
1090 * @param headerTranslation the translation amount of the header
1092 private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority,
1093 int headerTranslation) {
1094 if (!likeHighPriority && showingAsLowPriority()) {
1095 return mNotificationHeaderLowPriority.getHeight();
1097 int minExpandHeight = mNotificationHeaderMargin + headerTranslation;
1098 int visibleChildren = 0;
1099 boolean firstChild = true;
1100 int childCount = mChildren.size();
1101 for (int i = 0; i < childCount; i++) {
1102 if (visibleChildren >= maxAllowedVisibleChildren) {
1106 minExpandHeight += mChildPadding;
1110 ExpandableNotificationRow child = mChildren.get(i);
1111 minExpandHeight += child.getSingleLineView().getHeight();
1114 minExpandHeight += mCollapsedBottompadding;
1115 return minExpandHeight;
1118 public boolean showingAsLowPriority() {
1119 return mIsLowPriority && !mContainingNotification.isExpanded();
1122 public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
1123 if (mNotificationHeader != null) {
1124 removeView(mNotificationHeader);
1125 mNotificationHeader = null;
1127 if (mNotificationHeaderLowPriority != null) {
1128 removeView(mNotificationHeaderLowPriority);
1129 mNotificationHeaderLowPriority = null;
1131 recreateNotificationHeader(listener);
1133 for (int i = 0; i < mDividers.size(); i++) {
1134 View prevDivider = mDividers.get(i);
1135 int index = indexOfChild(prevDivider);
1136 removeView(prevDivider);
1137 View divider = inflateDivider();
1138 addView(divider, index);
1139 mDividers.set(i, divider);
1141 removeView(mOverflowNumber);
1142 mOverflowNumber = null;
1143 mGroupOverFlowState = null;
1144 updateGroupOverflow();
1147 public void setUserLocked(boolean userLocked) {
1148 mUserLocked = userLocked;
1150 updateHeaderVisibility(false /* animate */);
1152 int childCount = mChildren.size();
1153 for (int i = 0; i < childCount; i++) {
1154 ExpandableNotificationRow child = mChildren.get(i);
1155 child.setUserLocked(userLocked && !showingAsLowPriority());
1157 updateHeaderTouchability();
1160 private void updateHeaderTouchability() {
1161 if (mNotificationHeader != null) {
1162 mNotificationHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked);
1166 public void onNotificationUpdated() {
1167 mHybridGroupManager.setOverflowNumberColor(mOverflowNumber,
1168 mContainingNotification.getNotificationColor());
1171 public int getPositionInLinearLayout(View childInGroup) {
1172 int position = mNotificationHeaderMargin + mCurrentHeaderTranslation
1173 + mNotificatonTopPadding;
1175 for (int i = 0; i < mChildren.size(); i++) {
1176 ExpandableNotificationRow child = mChildren.get(i);
1177 boolean notGone = child.getVisibility() != View.GONE;
1179 position += mDividerHeight;
1181 if (child == childInGroup) {
1185 position += child.getIntrinsicHeight();
1191 public void setIconsVisible(boolean iconsVisible) {
1192 if (mNotificationHeaderWrapper != null) {
1193 NotificationHeaderView header = mNotificationHeaderWrapper.getNotificationHeader();
1194 if (header != null) {
1195 header.getIcon().setForceHidden(!iconsVisible);
1198 if (mNotificationHeaderWrapperLowPriority != null) {
1199 NotificationHeaderView header
1200 = mNotificationHeaderWrapperLowPriority.getNotificationHeader();
1201 if (header != null) {
1202 header.getIcon().setForceHidden(!iconsVisible);
1207 public void setClipBottomAmount(int clipBottomAmount) {
1208 mClipBottomAmount = clipBottomAmount;
1209 updateChildrenClipping();
1212 public void setIsLowPriority(boolean isLowPriority) {
1213 mIsLowPriority = isLowPriority;
1214 if (mContainingNotification != null) { /* we're not yet set up yet otherwise */
1215 recreateLowPriorityHeader(null /* existingBuilder */);
1216 updateHeaderVisibility(false /* animate */);
1219 setUserLocked(mUserLocked);
1223 public NotificationHeaderView getVisibleHeader() {
1224 NotificationHeaderView header = mNotificationHeader;
1225 if (showingAsLowPriority()) {
1226 header = mNotificationHeaderLowPriority;
1231 public void onExpansionChanged() {
1232 if (mIsLowPriority) {
1234 setUserLocked(mUserLocked);
1236 updateHeaderVisibility(true /* animate */);
1240 public float getIncreasedPaddingAmount() {
1241 if (showingAsLowPriority()) {
1244 return getGroupExpandFraction();
1248 public boolean isUserLocked() {
1252 public void setCurrentBottomRoundness(float currentBottomRoundness) {
1253 boolean last = true;
1254 for (int i = mChildren.size() - 1; i >= 0; i--) {
1255 ExpandableNotificationRow child = mChildren.get(i);
1256 if (child.getVisibility() == View.GONE) {
1259 float bottomRoundness = last ? currentBottomRoundness : 0.0f;
1260 child.setBottomRoundness(bottomRoundness, isShown() /* animate */);
1265 public void setHeaderVisibleAmount(float headerVisibleAmount) {
1266 mHeaderVisibleAmount = headerVisibleAmount;
1267 mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader);