/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar.stack; import android.app.Notification; import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.ColorDrawable; import android.service.notification.StatusBarNotification; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.NotificationHeaderView; import android.view.View; import android.view.ViewGroup; import android.widget.RemoteViews; import android.widget.TextView; import com.android.systemui.R; import com.android.systemui.ViewInvertHelper; import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.ExpandableNotificationRow; import com.android.systemui.statusbar.NotificationHeaderUtil; import com.android.systemui.statusbar.notification.HybridGroupManager; import com.android.systemui.statusbar.notification.HybridNotificationView; import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.NotificationViewWrapper; import com.android.systemui.statusbar.phone.NotificationPanelView; import java.util.ArrayList; import java.util.List; /** * A container containing child notifications */ public class NotificationChildrenContainer extends ViewGroup { private static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2; private static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5; private static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8; private final List<View> mDividers = new ArrayList<>(); private final List<ExpandableNotificationRow> mChildren = new ArrayList<>(); private final HybridGroupManager mHybridGroupManager; private int mChildPadding; private int mDividerHeight; private int mMaxNotificationHeight; private int mNotificationHeaderMargin; private int mNotificatonTopPadding; private float mCollapsedBottompadding; private ViewInvertHelper mOverflowInvertHelper; private boolean mChildrenExpanded; private ExpandableNotificationRow mNotificationParent; private TextView mOverflowNumber; private ViewState mGroupOverFlowState; private int mRealHeight; private boolean mUserLocked; private int mActualHeight; private boolean mNeverAppliedGroupState; private int mHeaderHeight; private NotificationHeaderView mNotificationHeader; private NotificationViewWrapper mNotificationHeaderWrapper; private NotificationHeaderUtil mHeaderUtil; private ViewState mHeaderViewState; public NotificationChildrenContainer(Context context) { this(context, null); } public NotificationChildrenContainer(Context context, AttributeSet attrs) { this(context, attrs, 0); } public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public NotificationChildrenContainer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initDimens(); mHybridGroupManager = new HybridGroupManager(getContext(), this); } private void initDimens() { mChildPadding = getResources().getDimensionPixelSize( R.dimen.notification_children_padding); mDividerHeight = Math.max(1, getResources().getDimensionPixelSize( R.dimen.notification_divider_height)); mHeaderHeight = getResources().getDimensionPixelSize(R.dimen.notification_header_height); mMaxNotificationHeight = getResources().getDimensionPixelSize( R.dimen.notification_max_height); mNotificationHeaderMargin = getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_content_margin_top); mNotificatonTopPadding = getResources().getDimensionPixelSize( R.dimen.notification_children_container_top_padding); mCollapsedBottompadding = getResources().getDimensionPixelSize( com.android.internal.R.dimen.notification_content_margin_bottom); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); for (int i = 0; i < childCount; i++) { View child = mChildren.get(i); // We need to layout all children even the GONE ones, such that the heights are // calculated correctly as they are used to calculate how many we can fit on the screen child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight); } if (mOverflowNumber != null) { boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; int left = (isRtl ? 0 : getWidth() - mOverflowNumber.getMeasuredWidth()); int right = left + mOverflowNumber.getMeasuredWidth(); mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight()); } if (mNotificationHeader != null) { mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(), mNotificationHeader.getMeasuredHeight()); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int ownMaxHeight = mMaxNotificationHeight; int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; int size = MeasureSpec.getSize(heightMeasureSpec); if (hasFixedHeight || isHeightLimited) { ownMaxHeight = Math.min(ownMaxHeight, size); } int newHeightSpec = MeasureSpec.makeMeasureSpec(ownMaxHeight, MeasureSpec.AT_MOST); int width = MeasureSpec.getSize(widthMeasureSpec); if (mOverflowNumber != null) { mOverflowNumber.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), newHeightSpec); } int dividerHeightSpec = MeasureSpec.makeMeasureSpec(mDividerHeight, MeasureSpec.EXACTLY); int height = mNotificationHeaderMargin + mNotificatonTopPadding; int childCount = Math.min(mChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); int collapsedChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); int overflowIndex = childCount > collapsedChildren ? collapsedChildren - 1 : -1; for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); // We need to measure all children even the GONE ones, such that the heights are // calculated correctly as they are used to calculate how many we can fit on the screen. boolean isOverflow = i == overflowIndex; child.setSingleLineWidthIndention(isOverflow && mOverflowNumber != null ? mOverflowNumber.getMeasuredWidth() : 0); child.measure(widthMeasureSpec, newHeightSpec); // layout the divider View divider = mDividers.get(i); divider.measure(widthMeasureSpec, dividerHeightSpec); if (child.getVisibility() != GONE) { height += child.getMeasuredHeight() + mDividerHeight; } } mRealHeight = height; if (heightMode != MeasureSpec.UNSPECIFIED) { height = Math.min(height, size); } if (mNotificationHeader != null) { int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec); } setMeasuredDimension(width, height); } @Override public boolean pointInView(float localX, float localY, float slop) { return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) && localY < (mRealHeight + slop); } /** * Add a child notification to this view. * * @param row the row to add * @param childIndex the index to add it at, if -1 it will be added at the end */ public void addNotification(ExpandableNotificationRow row, int childIndex) { int newIndex = childIndex < 0 ? mChildren.size() : childIndex; mChildren.add(newIndex, row); addView(row); row.setUserLocked(mUserLocked); View divider = inflateDivider(); addView(divider); mDividers.add(newIndex, divider); updateGroupOverflow(); } public void removeNotification(ExpandableNotificationRow row) { int childIndex = mChildren.indexOf(row); mChildren.remove(row); removeView(row); final View divider = mDividers.remove(childIndex); removeView(divider); getOverlay().add(divider); CrossFadeHelper.fadeOut(divider, new Runnable() { @Override public void run() { getOverlay().remove(divider); } }); row.setSystemChildExpanded(false); row.setUserLocked(false); updateGroupOverflow(); if (!row.isRemoved()) { mHeaderUtil.restoreNotificationHeader(row); } } /** * @return The number of notification children in the container. */ public int getNotificationChildCount() { return mChildren.size(); } public void recreateNotificationHeader(OnClickListener listener, StatusBarNotification notification) { final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(), mNotificationParent.getStatusBarNotification().getNotification()); final RemoteViews header = builder.makeNotificationHeader(); if (mNotificationHeader == null) { mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this); final View expandButton = mNotificationHeader.findViewById( com.android.internal.R.id.expand_button); expandButton.setVisibility(VISIBLE); mNotificationHeader.setOnClickListener(listener); mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(), mNotificationHeader, mNotificationParent); addView(mNotificationHeader, 0); invalidate(); } else { header.reapply(getContext(), mNotificationHeader); mNotificationHeaderWrapper.notifyContentUpdated(notification); } updateChildrenHeaderAppearance(); } public void updateChildrenHeaderAppearance() { mHeaderUtil.updateChildrenHeaderAppearance(); } public void updateGroupOverflow() { int childCount = mChildren.size(); int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); if (childCount > maxAllowedVisibleChildren) { mOverflowNumber = mHybridGroupManager.bindOverflowNumber( mOverflowNumber, childCount - maxAllowedVisibleChildren); if (mOverflowInvertHelper == null) { mOverflowInvertHelper = new ViewInvertHelper(mOverflowNumber, NotificationPanelView.DOZE_ANIMATION_DURATION); } if (mGroupOverFlowState == null) { mGroupOverFlowState = new ViewState(); mNeverAppliedGroupState = true; } } else if (mOverflowNumber != null) { removeView(mOverflowNumber); if (isShown()) { final View removedOverflowNumber = mOverflowNumber; addTransientView(removedOverflowNumber, getTransientViewCount()); CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() { @Override public void run() { removeTransientView(removedOverflowNumber); } }); } mOverflowNumber = null; mOverflowInvertHelper = null; mGroupOverFlowState = null; } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateGroupOverflow(); } private View inflateDivider() { return LayoutInflater.from(mContext).inflate( R.layout.notification_children_divider, this, false); } public List<ExpandableNotificationRow> getNotificationChildren() { return mChildren; } /** * Apply the order given in the list to the children. * * @param childOrder the new list order * @return whether the list order has changed */ public boolean applyChildOrder(List<ExpandableNotificationRow> childOrder) { if (childOrder == null) { return false; } boolean result = false; for (int i = 0; i < mChildren.size() && i < childOrder.size(); i++) { ExpandableNotificationRow child = mChildren.get(i); ExpandableNotificationRow desiredChild = childOrder.get(i); if (child != desiredChild) { mChildren.remove(desiredChild); mChildren.add(i, desiredChild); result = true; } } updateExpansionStates(); return result; } private void updateExpansionStates() { if (mChildrenExpanded || mUserLocked) { // we don't modify it the group is expanded or if we are expanding it return; } int size = mChildren.size(); for (int i = 0; i < size; i++) { ExpandableNotificationRow child = mChildren.get(i); child.setSystemChildExpanded(i == 0 && size == 1); } } /** * * @return the intrinsic size of this children container, i.e the natural fully expanded state */ public int getIntrinsicHeight() { int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); return getIntrinsicHeight(maxAllowedVisibleChildren); } /** * @return the intrinsic height with a number of children given * in @param maxAllowedVisibleChildren */ private int getIntrinsicHeight(float maxAllowedVisibleChildren) { int intrinsicHeight = mNotificationHeaderMargin; int visibleChildren = 0; int childCount = mChildren.size(); boolean firstChild = true; float expandFactor = 0; if (mUserLocked) { expandFactor = getGroupExpandFraction(); } for (int i = 0; i < childCount; i++) { if (visibleChildren >= maxAllowedVisibleChildren) { break; } if (!firstChild) { if (mUserLocked) { intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight, expandFactor); } else { intrinsicHeight += mChildrenExpanded ? mDividerHeight : mChildPadding; } } else { if (mUserLocked) { intrinsicHeight += NotificationUtils.interpolate( 0, mNotificatonTopPadding + mDividerHeight, expandFactor); } else { intrinsicHeight += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0; } firstChild = false; } ExpandableNotificationRow child = mChildren.get(i); intrinsicHeight += child.getIntrinsicHeight(); visibleChildren++; } if (mUserLocked) { intrinsicHeight += NotificationUtils.interpolate(mCollapsedBottompadding, 0.0f, expandFactor); } else if (!mChildrenExpanded) { intrinsicHeight += mCollapsedBottompadding; } return intrinsicHeight; } /** * Update the state of all its children based on a linear layout algorithm. * * @param resultState the state to update * @param parentState the state of the parent */ public void getState(StackScrollState resultState, StackViewState parentState) { int childCount = mChildren.size(); int yPosition = mNotificationHeaderMargin; boolean firstChild = true; int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(); int lastVisibleIndex = maxAllowedVisibleChildren - 1; int firstOverflowIndex = lastVisibleIndex + 1; float expandFactor = 0; if (mUserLocked) { expandFactor = getGroupExpandFraction(); firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */); } boolean childrenExpanded = !mNotificationParent.isGroupExpansionChanging() && mChildrenExpanded; int parentHeight = parentState.height; for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); if (!firstChild) { if (mUserLocked) { yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight, expandFactor); } else { yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding; } } else { if (mUserLocked) { yPosition += NotificationUtils.interpolate( 0, mNotificatonTopPadding + mDividerHeight, expandFactor); } else { yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0; } firstChild = false; } StackViewState childState = resultState.getViewStateForView(child); int intrinsicHeight = child.getIntrinsicHeight(); if (childrenExpanded) { // When a group is expanded and moving into bottom stack, the bottom visible child // adjusts its height to move into it. Children after it are hidden. if (updateChildStateForExpandedGroup(child, parentHeight, childState, yPosition)) { // Clipping might be deactivated if the view is transforming, however, clipping // the child into the bottom stack should take precedent over this. childState.isBottomClipped = true; } } else { childState.hidden = false; childState.height = intrinsicHeight; childState.isBottomClipped = false; } childState.yTranslation = yPosition; // When the group is expanded, the children cast the shadows rather than the parent // so use the parent's elevation here. childState.zTranslation = childrenExpanded ? mNotificationParent.getTranslationZ() : 0; childState.dimmed = parentState.dimmed; childState.dark = parentState.dark; childState.hideSensitive = parentState.hideSensitive; childState.belowSpeedBump = parentState.belowSpeedBump; childState.clipTopAmount = 0; childState.alpha = 0; if (i < firstOverflowIndex) { childState.alpha = 1; } else if (expandFactor == 1.0f && i <= lastVisibleIndex) { childState.alpha = (mActualHeight - childState.yTranslation) / childState.height; childState.alpha = Math.max(0.0f, Math.min(1.0f, childState.alpha)); } childState.location = parentState.location; yPosition += intrinsicHeight; } if (mOverflowNumber != null) { ExpandableNotificationRow overflowView = mChildren.get(Math.min( getMaxAllowedVisibleChildren(true /* likeCollpased */), childCount) - 1); mGroupOverFlowState.copyFrom(resultState.getViewStateForView(overflowView)); if (!mChildrenExpanded) { if (mUserLocked) { HybridNotificationView singleLineView = overflowView.getSingleLineView(); View mirrorView = singleLineView.getTextView(); if (mirrorView.getVisibility() == GONE) { mirrorView = singleLineView.getTitleView(); } if (mirrorView.getVisibility() == GONE) { mirrorView = singleLineView; } mGroupOverFlowState.yTranslation += NotificationUtils.getRelativeYOffset( mirrorView, overflowView); mGroupOverFlowState.alpha = mirrorView.getAlpha(); } } else { mGroupOverFlowState.yTranslation += mNotificationHeaderMargin; mGroupOverFlowState.alpha = 0.0f; } } if (mNotificationHeader != null) { if (mHeaderViewState == null) { mHeaderViewState = new ViewState(); } mHeaderViewState.initFrom(mNotificationHeader); mHeaderViewState.zTranslation = childrenExpanded ? mNotificationParent.getTranslationZ() : 0; } } /** * When moving into the bottom stack, the bottom visible child in an expanded group adjusts its * height, children in the group after this are gone. * * @param child the child who's height to adjust. * @param parentHeight the height of the parent. * @param childState the state to update. * @param yPosition the yPosition of the view. * @return true if children after this one should be hidden. */ private boolean updateChildStateForExpandedGroup(ExpandableNotificationRow child, int parentHeight, StackViewState childState, int yPosition) { final int top = yPosition + child.getClipTopAmount(); final int intrinsicHeight = child.getIntrinsicHeight(); final int bottom = top + intrinsicHeight; int newHeight = intrinsicHeight; if (bottom >= parentHeight) { // Child is either clipped or gone newHeight = Math.max((parentHeight - top), 0); } childState.hidden = newHeight == 0; childState.height = newHeight; return childState.height != intrinsicHeight && !childState.hidden; } private int getMaxAllowedVisibleChildren() { return getMaxAllowedVisibleChildren(false /* likeCollapsed */); } private int getMaxAllowedVisibleChildren(boolean likeCollapsed) { if (!likeCollapsed && (mChildrenExpanded || mNotificationParent.isUserLocked())) { return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; } if (!mNotificationParent.isOnKeyguard() && (mNotificationParent.isExpanded() || mNotificationParent.isHeadsUp())) { return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED; } return NUMBER_OF_CHILDREN_WHEN_COLLAPSED; } public void applyState(StackScrollState state) { int childCount = mChildren.size(); ViewState tmpState = new ViewState(); float expandFraction = 0.0f; if (mUserLocked) { expandFraction = getGroupExpandFraction(); } final boolean dividersVisible = mUserLocked || mNotificationParent.isGroupExpansionChanging(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); StackViewState viewState = state.getViewStateForView(child); state.applyState(child, viewState); // layout the divider View divider = mDividers.get(i); tmpState.initFrom(divider); tmpState.yTranslation = viewState.yTranslation - mDividerHeight; float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0; if (mUserLocked && viewState.alpha != 0) { alpha = NotificationUtils.interpolate(0, 0.5f, Math.min(viewState.alpha, expandFraction)); } tmpState.hidden = !dividersVisible; tmpState.alpha = alpha; state.applyViewState(divider, tmpState); // There is no fake shadow to be drawn on the children child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); } if (mOverflowNumber != null) { state.applyViewState(mOverflowNumber, mGroupOverFlowState); mNeverAppliedGroupState = false; } if (mNotificationHeader != null) { state.applyViewState(mNotificationHeader, mHeaderViewState); } } /** * This is called when the children expansion has changed and positions the children properly * for an appear animation. * * @param state the new state we animate to */ public void prepareExpansionChanged(StackScrollState state) { // TODO: do something that makes sense, like placing the invisible views correctly return; } public void startAnimationToState(StackScrollState state, StackStateAnimator stateAnimator, long baseDelay, long duration) { int childCount = mChildren.size(); ViewState tmpState = new ViewState(); float expandFraction = getGroupExpandFraction(); final boolean dividersVisible = mUserLocked || mNotificationParent.isGroupExpansionChanging(); for (int i = childCount - 1; i >= 0; i--) { ExpandableNotificationRow child = mChildren.get(i); StackViewState viewState = state.getViewStateForView(child); stateAnimator.startStackAnimations(child, viewState, state, -1, baseDelay); // layout the divider View divider = mDividers.get(i); tmpState.initFrom(divider); tmpState.yTranslation = viewState.yTranslation - mDividerHeight; float alpha = mChildrenExpanded && viewState.alpha != 0 ? 0.5f : 0; if (mUserLocked && viewState.alpha != 0) { alpha = NotificationUtils.interpolate(0, 0.5f, Math.min(viewState.alpha, expandFraction)); } tmpState.hidden = !dividersVisible; tmpState.alpha = alpha; stateAnimator.startViewAnimations(divider, tmpState, baseDelay, duration); // There is no fake shadow to be drawn on the children child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); } if (mOverflowNumber != null) { if (mNeverAppliedGroupState) { float alpha = mGroupOverFlowState.alpha; mGroupOverFlowState.alpha = 0; state.applyViewState(mOverflowNumber, mGroupOverFlowState); mGroupOverFlowState.alpha = alpha; mNeverAppliedGroupState = false; } stateAnimator.startViewAnimations(mOverflowNumber, mGroupOverFlowState, baseDelay, duration); } if (mNotificationHeader != null) { state.applyViewState(mNotificationHeader, mHeaderViewState); } } public ExpandableNotificationRow getViewAtPosition(float y) { // find the view under the pointer, accounting for GONE views final int count = mChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { ExpandableNotificationRow slidingChild = mChildren.get(childIdx); float childTop = slidingChild.getTranslationY(); float top = childTop + slidingChild.getClipTopAmount(); float bottom = childTop + slidingChild.getActualHeight(); if (y >= top && y <= bottom) { return slidingChild; } } return null; } public void setChildrenExpanded(boolean childrenExpanded) { mChildrenExpanded = childrenExpanded; updateExpansionStates(); if (mNotificationHeader != null) { mNotificationHeader.setExpanded(childrenExpanded); } final int count = mChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { ExpandableNotificationRow child = mChildren.get(childIdx); child.setChildrenExpanded(childrenExpanded, false); } } public void setNotificationParent(ExpandableNotificationRow parent) { mNotificationParent = parent; mHeaderUtil = new NotificationHeaderUtil(mNotificationParent); } public ExpandableNotificationRow getNotificationParent() { return mNotificationParent; } public NotificationHeaderView getHeaderView() { return mNotificationHeader; } public void updateHeaderVisibility(int visiblity) { if (mNotificationHeader != null) { mNotificationHeader.setVisibility(visiblity); } } /** * Called when a groups expansion changes to adjust the background of the header view. * * @param expanded whether the group is expanded. */ public void updateHeaderForExpansion(boolean expanded) { if (mNotificationHeader != null) { if (expanded) { ColorDrawable cd = new ColorDrawable(); cd.setColor(mNotificationParent.calculateBgColor()); mNotificationHeader.setHeaderBackgroundDrawable(cd); } else { mNotificationHeader.setHeaderBackgroundDrawable(null); } } } public int getMaxContentHeight() { int maxContentHeight = mNotificationHeaderMargin + mNotificatonTopPadding; int visibleChildren = 0; int childCount = mChildren.size(); for (int i = 0; i < childCount; i++) { if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) { break; } ExpandableNotificationRow child = mChildren.get(i); float childHeight = child.isExpanded(true /* allowOnKeyguard */) ? child.getMaxExpandHeight() : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); maxContentHeight += childHeight; visibleChildren++; } if (visibleChildren > 0) { maxContentHeight += visibleChildren * mDividerHeight; } return maxContentHeight; } public void setActualHeight(int actualHeight) { if (!mUserLocked) { return; } mActualHeight = actualHeight; float fraction = getGroupExpandFraction(); int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); int childCount = mChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); float childHeight = child.isExpanded(true /* allowOnKeyguard */) ? child.getMaxExpandHeight() : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); if (i < maxAllowedVisibleChildren) { float singleLineHeight = child.getShowingLayout().getMinHeight( false /* likeGroupExpanded */); child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight, childHeight, fraction), false); } else { child.setActualHeight((int) childHeight, false); } } } public float getGroupExpandFraction() { int visibleChildrenExpandedHeight = getVisibleChildrenExpandHeight(); int minExpandHeight = getCollapsedHeight(); float factor = (mActualHeight - minExpandHeight) / (float) (visibleChildrenExpandedHeight - minExpandHeight); return Math.max(0.0f, Math.min(1.0f, factor)); } private int getVisibleChildrenExpandHeight() { int intrinsicHeight = mNotificationHeaderMargin + mNotificatonTopPadding + mDividerHeight; int visibleChildren = 0; int childCount = mChildren.size(); int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); for (int i = 0; i < childCount; i++) { if (visibleChildren >= maxAllowedVisibleChildren) { break; } ExpandableNotificationRow child = mChildren.get(i); float childHeight = child.isExpanded(true /* allowOnKeyguard */) ? child.getMaxExpandHeight() : child.getShowingLayout().getMinHeight(true /* likeGroupExpanded */); intrinsicHeight += childHeight; visibleChildren++; } return intrinsicHeight; } public int getMinHeight() { return getMinHeight(NUMBER_OF_CHILDREN_WHEN_COLLAPSED); } public int getCollapsedHeight() { return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */)); } private int getMinHeight(int maxAllowedVisibleChildren) { int minExpandHeight = mNotificationHeaderMargin; int visibleChildren = 0; boolean firstChild = true; int childCount = mChildren.size(); for (int i = 0; i < childCount; i++) { if (visibleChildren >= maxAllowedVisibleChildren) { break; } if (!firstChild) { minExpandHeight += mChildPadding; } else { firstChild = false; } ExpandableNotificationRow child = mChildren.get(i); minExpandHeight += child.getSingleLineView().getHeight(); visibleChildren++; } minExpandHeight += mCollapsedBottompadding; return minExpandHeight; } public void setDark(boolean dark, boolean fade, long delay) { if (mOverflowNumber != null) { mOverflowInvertHelper.setInverted(dark, fade, delay); } mNotificationHeaderWrapper.setDark(dark, fade, delay); } public void reInflateViews(OnClickListener listener, StatusBarNotification notification) { removeView(mNotificationHeader); mNotificationHeader = null; recreateNotificationHeader(listener, notification); initDimens(); for (int i = 0; i < mDividers.size(); i++) { View prevDivider = mDividers.get(i); int index = indexOfChild(prevDivider); removeView(prevDivider); View divider = inflateDivider(); addView(divider, index); mDividers.set(i, divider); } removeView(mOverflowNumber); mOverflowNumber = null; mOverflowInvertHelper = null; mGroupOverFlowState = null; updateGroupOverflow(); } public void setUserLocked(boolean userLocked) { mUserLocked = userLocked; int childCount = mChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); child.setUserLocked(userLocked); } } public void onNotificationUpdated() { mHybridGroupManager.setOverflowNumberColor(mOverflowNumber, mNotificationParent.getNotificationColor()); } public int getPositionInLinearLayout(View childInGroup) { int position = mNotificationHeaderMargin + mNotificatonTopPadding; for (int i = 0; i < mChildren.size(); i++) { ExpandableNotificationRow child = mChildren.get(i); boolean notGone = child.getVisibility() != View.GONE; if (notGone) { position += mDividerHeight; } if (child == childInGroup) { return position; } if (notGone) { position += child.getIntrinsicHeight(); } } return 0; } }