/* * 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.content.res.Resources; 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.internal.annotations.VisibleForTesting; import com.android.systemui.R; 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.notification.VisualStabilityManager; 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 static final int NUMBER_OF_CHILDREN_WHEN_AMBIENT = 3; private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() { private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha(); @Override public AnimationFilter getAnimationFilter() { return mAnimationFilter; } }.setDuration(200); private final List mDividers = new ArrayList<>(); private final List mChildren = new ArrayList<>(); private final HybridGroupManager mHybridGroupManager; private int mChildPadding; private int mDividerHeight; private float mDividerAlpha; private int mNotificationHeaderMargin; private int mNotificatonTopPadding; private float mCollapsedBottompadding; private boolean mChildrenExpanded; private ExpandableNotificationRow mContainingNotification; private TextView mOverflowNumber; private ViewState mGroupOverFlowState; private int mRealHeight; private boolean mUserLocked; private int mActualHeight; private boolean mNeverAppliedGroupState; private int mHeaderHeight; /** * Whether or not individual notifications that are part of this container will have shadows. */ private boolean mEnableShadowOnChildNotifications; private NotificationHeaderView mNotificationHeader; private NotificationViewWrapper mNotificationHeaderWrapper; private NotificationHeaderView mNotificationHeaderLowPriority; private NotificationViewWrapper mNotificationHeaderWrapperLowPriority; private ViewGroup mNotificationHeaderAmbient; private NotificationViewWrapper mNotificationHeaderWrapperAmbient; private NotificationHeaderUtil mHeaderUtil; private ViewState mHeaderViewState; private int mClipBottomAmount; private boolean mIsLowPriority; private OnClickListener mHeaderClickListener; private ViewGroup mCurrentHeader; private boolean mShowDividersWhenExpanded; private boolean mHideDividersDuringExpand; 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() { Resources res = getResources(); mChildPadding = res.getDimensionPixelSize(R.dimen.notification_children_padding); mDividerHeight = res.getDimensionPixelSize( R.dimen.notification_children_container_divider_height); mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha); mHeaderHeight = res.getDimensionPixelSize( R.dimen.notification_children_container_header_height); mNotificationHeaderMargin = res.getDimensionPixelSize( R.dimen.notification_children_container_margin_top); mNotificatonTopPadding = res.getDimensionPixelSize( R.dimen.notification_children_container_top_padding); mCollapsedBottompadding = res.getDimensionPixelSize( com.android.internal.R.dimen.notification_content_margin_bottom); mEnableShadowOnChildNotifications = res.getBoolean(R.bool.config_enableShadowOnChildNotifications); mShowDividersWhenExpanded = res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded); mHideDividersDuringExpand = res.getBoolean(R.bool.config_hideDividersDuringExpand); } @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()); } if (mNotificationHeaderLowPriority != null) { mNotificationHeaderLowPriority.layout(0, 0, mNotificationHeaderLowPriority.getMeasuredWidth(), mNotificationHeaderLowPriority.getMeasuredHeight()); } if (mNotificationHeaderAmbient != null) { mNotificationHeaderAmbient.layout(0, 0, mNotificationHeaderAmbient.getMeasuredWidth(), mNotificationHeaderAmbient.getMeasuredHeight()); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY; boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST; int size = MeasureSpec.getSize(heightMeasureSpec); int newHeightSpec = heightMeasureSpec; if (hasFixedHeight || isHeightLimited) { newHeightSpec = MeasureSpec.makeMeasureSpec(size, 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); } int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); if (mNotificationHeader != null) { mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec); } if (mNotificationHeaderLowPriority != null) { headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec); } if (mNotificationHeaderAmbient != null) { headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); mNotificationHeaderAmbient.measure(widthMeasureSpec, headerHeightSpec); } setMeasuredDimension(width, height); } @Override public boolean hasOverlappingRendering() { return false; } @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(); row.setContentTransformationAmount(0, false /* isLastChild */); } 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) { mHeaderClickListener = listener; StatusBarNotification notification = mContainingNotification.getStatusBarNotification(); final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); RemoteViews header = builder.makeNotificationHeader(false /* ambient */); 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(mHeaderClickListener); mNotificationHeaderWrapper = NotificationViewWrapper.wrap(getContext(), mNotificationHeader, mContainingNotification); addView(mNotificationHeader, 0); invalidate(); } else { header.reapply(getContext(), mNotificationHeader); } mNotificationHeaderWrapper.onContentUpdated(mContainingNotification); recreateLowPriorityHeader(builder); recreateAmbientHeader(builder); updateHeaderVisibility(false /* animate */); updateChildrenHeaderAppearance(); } private void recreateAmbientHeader(Notification.Builder builder) { RemoteViews header; StatusBarNotification notification = mContainingNotification.getStatusBarNotification(); if (builder == null) { builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); } header = builder.makeNotificationHeader(true /* ambient */); if (mNotificationHeaderAmbient == null) { mNotificationHeaderAmbient = (ViewGroup) header.apply(getContext(), this); mNotificationHeaderWrapperAmbient = NotificationViewWrapper.wrap(getContext(), mNotificationHeaderAmbient, mContainingNotification); mNotificationHeaderWrapperAmbient.onContentUpdated(mContainingNotification); addView(mNotificationHeaderAmbient, 0); invalidate(); } else { header.reapply(getContext(), mNotificationHeaderAmbient); } resetHeaderVisibilityIfNeeded(mNotificationHeaderAmbient, calculateDesiredHeader()); mNotificationHeaderWrapperAmbient.onContentUpdated(mContainingNotification); } /** * Recreate the low-priority header. * * @param builder a builder to reuse. Otherwise the builder will be recovered. */ private void recreateLowPriorityHeader(Notification.Builder builder) { RemoteViews header; StatusBarNotification notification = mContainingNotification.getStatusBarNotification(); if (mIsLowPriority) { if (builder == null) { builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); } header = builder.makeLowPriorityContentView(true /* useRegularSubtext */); if (mNotificationHeaderLowPriority == null) { mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(), this); final View expandButton = mNotificationHeaderLowPriority.findViewById( com.android.internal.R.id.expand_button); expandButton.setVisibility(VISIBLE); mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener); mNotificationHeaderWrapperLowPriority = NotificationViewWrapper.wrap(getContext(), mNotificationHeaderLowPriority, mContainingNotification); addView(mNotificationHeaderLowPriority, 0); invalidate(); } else { header.reapply(getContext(), mNotificationHeaderLowPriority); } mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification); resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader()); } else { removeView(mNotificationHeaderLowPriority); mNotificationHeaderLowPriority = null; mNotificationHeaderWrapperLowPriority = null; } } 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 (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; 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 getNotificationChildren() { return mChildren; } /** * Apply the order given in the list to the children. * * @param childOrder the new list order * @param visualStabilityManager * @param callback * @return whether the list order has changed */ public boolean applyChildOrder(List childOrder, VisualStabilityManager visualStabilityManager, VisualStabilityManager.Callback callback) { 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) { if (visualStabilityManager.canReorderNotification(desiredChild)) { mChildren.remove(desiredChild); mChildren.add(i, desiredChild); result = true; } else { visualStabilityManager.addReorderingAllowedCallback(callback); } } } 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) { if (showingAsLowPriority()) { return mNotificationHeaderLowPriority.getHeight(); } int intrinsicHeight = mNotificationHeaderMargin; int visibleChildren = 0; int childCount = mChildren.size(); boolean firstChild = true; float expandFactor = 0; if (mUserLocked) { expandFactor = getGroupExpandFraction(); } boolean childrenExpanded = mChildrenExpanded || mContainingNotification.isShowingAmbient(); for (int i = 0; i < childCount; i++) { if (visibleChildren >= maxAllowedVisibleChildren) { break; } if (!firstChild) { if (mUserLocked) { intrinsicHeight += NotificationUtils.interpolate(mChildPadding, mDividerHeight, expandFactor); } else { intrinsicHeight += childrenExpanded ? mDividerHeight : mChildPadding; } } else { if (mUserLocked) { intrinsicHeight += NotificationUtils.interpolate( 0, mNotificatonTopPadding + mDividerHeight, expandFactor); } else { intrinsicHeight += childrenExpanded ? 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 (!childrenExpanded) { 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, ExpandableViewState 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; boolean expandingToExpandedGroup = mUserLocked && !showingAsLowPriority(); if (mUserLocked) { expandFactor = getGroupExpandFraction(); firstOverflowIndex = getMaxAllowedVisibleChildren(true /* likeCollapsed */); } boolean childrenExpandedAndNotAnimating = mChildrenExpanded && !mContainingNotification.isGroupExpansionChanging(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); if (!firstChild) { if (expandingToExpandedGroup) { yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight, expandFactor); } else { yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding; } } else { if (expandingToExpandedGroup) { yPosition += NotificationUtils.interpolate( 0, mNotificatonTopPadding + mDividerHeight, expandFactor); } else { yPosition += mChildrenExpanded ? mNotificatonTopPadding + mDividerHeight : 0; } firstChild = false; } ExpandableViewState childState = resultState.getViewStateForView(child); int intrinsicHeight = child.getIntrinsicHeight(); childState.height = intrinsicHeight; childState.yTranslation = yPosition; childState.hidden = false; // When the group is expanded, the children cast the shadows rather than the parent // so use the parent's elevation here. childState.zTranslation = (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications) ? mContainingNotification.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 = showingAsLowPriority() ? expandFactor : 1.0f; } 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; childState.inShelf = parentState.inShelf; yPosition += intrinsicHeight; } if (mOverflowNumber != null) { ExpandableNotificationRow overflowView = mChildren.get(Math.min( getMaxAllowedVisibleChildren(true /* likeCollpased */), childCount) - 1); mGroupOverFlowState.copyFrom(resultState.getViewStateForView(overflowView)); if (mContainingNotification.isShowingAmbient() || !mChildrenExpanded) { HybridNotificationView alignView = null; if (mContainingNotification.isShowingAmbient()) { alignView = overflowView.getAmbientSingleLineView(); } else if (mUserLocked) { alignView = overflowView.getSingleLineView(); } if (alignView != null) { View mirrorView = alignView.getTextView(); if (mirrorView.getVisibility() == GONE) { mirrorView = alignView.getTitleView(); } if (mirrorView.getVisibility() == GONE) { mirrorView = alignView; } 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 = childrenExpandedAndNotAnimating ? mContainingNotification.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, ExpandableViewState 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 (mContainingNotification.isShowingAmbient()) { return NUMBER_OF_CHILDREN_WHEN_AMBIENT; } if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked())) { return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; } if (mIsLowPriority || !mContainingNotification.isOnKeyguard() && (mContainingNotification.isExpanded() || mContainingNotification.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 && !showingAsLowPriority() || (mChildrenExpanded && mShowDividersWhenExpanded) || (mContainingNotification.isGroupExpansionChanging() && !mHideDividersDuringExpand); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); ExpandableViewState viewState = state.getViewStateForView(child); viewState.applyToView(child); // layout the divider View divider = mDividers.get(i); tmpState.initFrom(divider); tmpState.yTranslation = viewState.yTranslation - mDividerHeight; float alpha = mChildrenExpanded && viewState.alpha != 0 ? mDividerAlpha : 0; if (mUserLocked && !showingAsLowPriority() && viewState.alpha != 0) { alpha = NotificationUtils.interpolate(0, 0.5f, Math.min(viewState.alpha, expandFraction)); } tmpState.hidden = !dividersVisible; tmpState.alpha = alpha; tmpState.applyToView(divider); // There is no fake shadow to be drawn on the children child.setFakeShadowIntensity(0.0f, 0.0f, 0, 0); } if (mGroupOverFlowState != null) { mGroupOverFlowState.applyToView(mOverflowNumber); mNeverAppliedGroupState = false; } if (mHeaderViewState != null) { mHeaderViewState.applyToView(mNotificationHeader); } updateChildrenClipping(); } private void updateChildrenClipping() { int childCount = mChildren.size(); int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount; for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); if (child.getVisibility() == GONE) { continue; } float childTop = child.getTranslationY(); float childBottom = childTop + child.getActualHeight(); boolean visible = true; int clipBottomAmount = 0; if (childTop > layoutEnd) { visible = false; } else if (childBottom > layoutEnd) { clipBottomAmount = (int) (childBottom - layoutEnd); } boolean isVisible = child.getVisibility() == VISIBLE; if (visible != isVisible) { child.setVisibility(visible ? VISIBLE : INVISIBLE); } child.setClipBottomAmount(clipBottomAmount); } } /** * 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, AnimationProperties properties) { int childCount = mChildren.size(); ViewState tmpState = new ViewState(); float expandFraction = getGroupExpandFraction(); final boolean dividersVisible = mUserLocked && !showingAsLowPriority() || (mChildrenExpanded && mShowDividersWhenExpanded) || (mContainingNotification.isGroupExpansionChanging() && !mHideDividersDuringExpand); for (int i = childCount - 1; i >= 0; i--) { ExpandableNotificationRow child = mChildren.get(i); ExpandableViewState viewState = state.getViewStateForView(child); viewState.animateTo(child, properties); // 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 && !showingAsLowPriority() && viewState.alpha != 0) { alpha = NotificationUtils.interpolate(0, 0.5f, Math.min(viewState.alpha, expandFraction)); } tmpState.hidden = !dividersVisible; tmpState.alpha = alpha; tmpState.animateTo(divider, properties); // 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; mGroupOverFlowState.applyToView(mOverflowNumber); mGroupOverFlowState.alpha = alpha; mNeverAppliedGroupState = false; } mGroupOverFlowState.animateTo(mOverflowNumber, properties); } if (mNotificationHeader != null) { mHeaderViewState.applyToView(mNotificationHeader); } updateChildrenClipping(); } 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 setContainingNotification(ExpandableNotificationRow parent) { mContainingNotification = parent; mHeaderUtil = new NotificationHeaderUtil(mContainingNotification); } public ExpandableNotificationRow getContainingNotification() { return mContainingNotification; } public NotificationHeaderView getHeaderView() { return mNotificationHeader; } public NotificationHeaderView getLowPriorityHeaderView() { return mNotificationHeaderLowPriority; } @VisibleForTesting public ViewGroup getCurrentHeaderView() { return mCurrentHeader; } public void notifyShowAmbientChanged() { updateHeaderVisibility(false); } private void updateHeaderVisibility(boolean animate) { ViewGroup desiredHeader; ViewGroup currentHeader = mCurrentHeader; desiredHeader = calculateDesiredHeader(); if (currentHeader == desiredHeader) { return; } if (desiredHeader == mNotificationHeaderAmbient || currentHeader == mNotificationHeaderAmbient) { animate = false; } if (animate) { if (desiredHeader != null && currentHeader != null) { currentHeader.setVisibility(VISIBLE); desiredHeader.setVisibility(VISIBLE); NotificationViewWrapper visibleWrapper = getWrapperForView(desiredHeader); NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader); visibleWrapper.transformFrom(hiddenWrapper); hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false)); startChildAlphaAnimations(desiredHeader == mNotificationHeader); } else { animate = false; } } if (!animate) { if (desiredHeader != null) { getWrapperForView(desiredHeader).setVisible(true); desiredHeader.setVisibility(VISIBLE); } if (currentHeader != null) { // Wrapper can be null if we were a low priority notification // and just destroyed it by calling setIsLowPriority(false) NotificationViewWrapper wrapper = getWrapperForView(currentHeader); if (wrapper != null) { wrapper.setVisible(false); } currentHeader.setVisibility(INVISIBLE); } } resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader); resetHeaderVisibilityIfNeeded(mNotificationHeaderAmbient, desiredHeader); resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader); mCurrentHeader = desiredHeader; } private void resetHeaderVisibilityIfNeeded(View header, View desiredHeader) { if (header == null) { return; } if (header != mCurrentHeader && header != desiredHeader) { getWrapperForView(header).setVisible(false); header.setVisibility(INVISIBLE); } if (header == desiredHeader && header.getVisibility() != VISIBLE) { getWrapperForView(header).setVisible(true); header.setVisibility(VISIBLE); } } private ViewGroup calculateDesiredHeader() { ViewGroup desiredHeader; if (mContainingNotification.isShowingAmbient()) { desiredHeader = mNotificationHeaderAmbient; } else if (showingAsLowPriority()) { desiredHeader = mNotificationHeaderLowPriority; } else { desiredHeader = mNotificationHeader; } return desiredHeader; } private void startChildAlphaAnimations(boolean toVisible) { float target = toVisible ? 1.0f : 0.0f; float start = 1.0f - target; int childCount = mChildren.size(); for (int i = 0; i < childCount; i++) { if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) { break; } ExpandableNotificationRow child = mChildren.get(i); child.setAlpha(start); ViewState viewState = new ViewState(); viewState.initFrom(child); viewState.alpha = target; ALPHA_FADE_IN.setDelay(i * 50); viewState.animateTo(child, ALPHA_FADE_IN); } } private void updateHeaderTransformation() { if (mUserLocked && showingAsLowPriority()) { float fraction = getGroupExpandFraction(); mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority, fraction); mNotificationHeader.setVisibility(VISIBLE); mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper, fraction); } } private NotificationViewWrapper getWrapperForView(View visibleHeader) { if (visibleHeader == mNotificationHeader) { return mNotificationHeaderWrapper; } if (visibleHeader == mNotificationHeaderAmbient) { return mNotificationHeaderWrapperAmbient; } return mNotificationHeaderWrapperLowPriority; } /** * 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(mContainingNotification.calculateBgColor()); mNotificationHeader.setHeaderBackgroundDrawable(cd); } else { mNotificationHeader.setHeaderBackgroundDrawable(null); } } } public int getMaxContentHeight() { if (showingAsLowPriority()) { return getMinHeight(NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED, true /* likeHighPriority */); } 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(); boolean showingLowPriority = showingAsLowPriority(); updateHeaderTransformation(); int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */); int childCount = mChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); float childHeight; if (showingLowPriority) { childHeight = child.getShowingLayout().getMinHeight(false /* likeGroupExpanded */); } else if (child.isExpanded(true /* allowOnKeyguard */)) { childHeight = child.getMaxExpandHeight(); } else { childHeight = 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 = showingAsLowPriority() ? getMaxContentHeight() : 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(mContainingNotification.isShowingAmbient() ? NUMBER_OF_CHILDREN_WHEN_AMBIENT : NUMBER_OF_CHILDREN_WHEN_COLLAPSED, false /* likeHighPriority */); } public int getCollapsedHeight() { return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */), false /* likeHighPriority */); } /** * Get the minimum Height for this group. * * @param maxAllowedVisibleChildren the number of children that should be visible * @param likeHighPriority if the height should be calculated as if it were not low priority */ private int getMinHeight(int maxAllowedVisibleChildren, boolean likeHighPriority) { if (!likeHighPriority && showingAsLowPriority()) { return mNotificationHeaderLowPriority.getHeight(); } 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 boolean showingAsLowPriority() { return mIsLowPriority && !mContainingNotification.isExpanded(); } public void setDark(boolean dark, boolean fade, long delay) { if (mOverflowNumber != null) { mHybridGroupManager.setOverflowNumberDark(mOverflowNumber, dark, fade, delay); } mNotificationHeaderWrapper.setDark(dark, fade, delay); } public void reInflateViews(OnClickListener listener, StatusBarNotification notification) { if (mNotificationHeader != null) { removeView(mNotificationHeader); mNotificationHeader = null; } if (mNotificationHeaderLowPriority != null) { removeView(mNotificationHeaderLowPriority); mNotificationHeaderLowPriority = null; } if (mNotificationHeaderAmbient != null) { removeView(mNotificationHeaderAmbient); mNotificationHeaderAmbient = null; } recreateNotificationHeader(listener); 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; mGroupOverFlowState = null; updateGroupOverflow(); } public void setUserLocked(boolean userLocked) { mUserLocked = userLocked; if (!mUserLocked) { updateHeaderVisibility(false /* animate */); } int childCount = mChildren.size(); for (int i = 0; i < childCount; i++) { ExpandableNotificationRow child = mChildren.get(i); child.setUserLocked(userLocked && !showingAsLowPriority()); } } public void onNotificationUpdated() { mHybridGroupManager.setOverflowNumberColor(mOverflowNumber, mContainingNotification.getNotificationColor(), mContainingNotification.getNotificationColorAmbient()); } 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; } public void setIconsVisible(boolean iconsVisible) { if (mNotificationHeaderWrapper != null) { NotificationHeaderView header = mNotificationHeaderWrapper.getNotificationHeader(); if (header != null) { header.getIcon().setForceHidden(!iconsVisible); } } if (mNotificationHeaderWrapperLowPriority != null) { NotificationHeaderView header = mNotificationHeaderWrapperLowPriority.getNotificationHeader(); if (header != null) { header.getIcon().setForceHidden(!iconsVisible); } } } public void setClipBottomAmount(int clipBottomAmount) { mClipBottomAmount = clipBottomAmount; updateChildrenClipping(); } public void setIsLowPriority(boolean isLowPriority) { mIsLowPriority = isLowPriority; if (mContainingNotification != null) { /* we're not yet set up yet otherwise */ recreateLowPriorityHeader(null /* existingBuilder */); updateHeaderVisibility(false /* animate */); } if (mUserLocked) { setUserLocked(mUserLocked); } } public NotificationHeaderView getVisibleHeader() { NotificationHeaderView header = mNotificationHeader; if (showingAsLowPriority()) { header = mNotificationHeaderLowPriority; } return header; } public void onExpansionChanged() { if (mIsLowPriority) { if (mUserLocked) { setUserLocked(mUserLocked); } updateHeaderVisibility(true /* animate */); } } public float getIncreasedPaddingAmount() { if (showingAsLowPriority()) { return 0.0f; } return getGroupExpandFraction(); } @VisibleForTesting public boolean isUserLocked() { return mUserLocked; } }