blob: d18757d3453fc77545506f259d67440b4aa0a3b6 [file] [log] [blame]
/*
* 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.notification.stack;
import android.app.Notification;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.drawable.ColorDrawable;
import android.os.Trace;
import android.service.notification.StatusBarNotification;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.NotificationExpandButton;
import com.android.systemui.R;
import com.android.systemui.statusbar.CrossFadeHelper;
import com.android.systemui.statusbar.NotificationGroupingUtil;
import com.android.systemui.statusbar.notification.FeedbackIcon;
import com.android.systemui.statusbar.notification.NotificationFadeAware;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.Roundable;
import com.android.systemui.statusbar.notification.RoundableState;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.HybridGroupManager;
import com.android.systemui.statusbar.notification.row.HybridNotificationView;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import java.util.ArrayList;
import java.util.List;
/**
* A container containing child notifications
*/
public class NotificationChildrenContainer extends ViewGroup
implements NotificationFadeAware, Roundable {
private static final String TAG = "NotificationChildrenContainer";
@VisibleForTesting
static final int NUMBER_OF_CHILDREN_WHEN_COLLAPSED = 2;
@VisibleForTesting
static final int NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED = 5;
public static final int NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED = 8;
private static final AnimationProperties ALPHA_FADE_IN = new AnimationProperties() {
private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
@Override
public AnimationFilter getAnimationFilter() {
return mAnimationFilter;
}
}.setDuration(200);
private static final SourceType FROM_PARENT = SourceType.from("FromParent(NCC)");
private final List<View> mDividers = new ArrayList<>();
private final List<ExpandableNotificationRow> mAttachedChildren = new ArrayList<>();
private final HybridGroupManager mHybridGroupManager;
private int mChildPadding;
private int mDividerHeight;
private float mDividerAlpha;
private int mNotificationHeaderMargin;
private int mNotificationTopPadding;
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 NotificationHeaderViewWrapper mNotificationHeaderWrapper;
private NotificationHeaderView mNotificationHeaderLowPriority;
private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority;
private NotificationGroupingUtil mGroupingUtil;
private ViewState mHeaderViewState;
private int mClipBottomAmount;
private boolean mIsLowPriority;
private OnClickListener mHeaderClickListener;
private ViewGroup mCurrentHeader;
private boolean mIsConversation;
private Path mChildClipPath = null;
private final Path mHeaderPath = new Path();
private boolean mShowGroupCountInExpander;
private boolean mShowDividersWhenExpanded;
private boolean mHideDividersDuringExpand;
private int mTranslationForHeader;
private int mCurrentHeaderTranslation = 0;
private float mHeaderVisibleAmount = 1.0f;
private int mUntruncatedChildCount;
private boolean mContainingNotificationIsFaded = false;
private RoundableState mRoundableState;
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);
mHybridGroupManager = new HybridGroupManager(getContext());
mRoundableState = new RoundableState(this, this, 0f);
initDimens();
setClipChildren(false);
}
private void initDimens() {
Resources res = getResources();
mChildPadding = res.getDimensionPixelOffset(R.dimen.notification_children_padding);
mDividerHeight = res.getDimensionPixelOffset(
R.dimen.notification_children_container_divider_height);
mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha);
mNotificationHeaderMargin = res.getDimensionPixelOffset(
R.dimen.notification_children_container_margin_top);
mNotificationTopPadding = res.getDimensionPixelOffset(
R.dimen.notification_children_container_top_padding);
mHeaderHeight = mNotificationHeaderMargin + mNotificationTopPadding;
mCollapsedBottomPadding = res.getDimensionPixelOffset(
R.dimen.notification_children_collapsed_bottom_padding);
mEnableShadowOnChildNotifications =
res.getBoolean(R.bool.config_enableShadowOnChildNotifications);
mShowGroupCountInExpander =
res.getBoolean(R.bool.config_showNotificationGroupCountInExpander);
mShowDividersWhenExpanded =
res.getBoolean(R.bool.config_showDividersWhenGroupNotificationExpanded);
mHideDividersDuringExpand =
res.getBoolean(R.bool.config_hideDividersDuringExpand);
mTranslationForHeader = res.getDimensionPixelOffset(
com.android.internal.R.dimen.notification_content_margin)
- mNotificationHeaderMargin;
mHybridGroupManager.initDimens();
}
@NonNull
@Override
public RoundableState getRoundableState() {
return mRoundableState;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount =
Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
for (int i = 0; i < childCount; i++) {
View child = mAttachedChildren.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());
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Trace.beginSection("NotificationChildrenContainer#onMeasure");
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 + mNotificationTopPadding;
int childCount =
Math.min(mAttachedChildren.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 = mAttachedChildren.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) {
mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec);
}
setMeasuredDimension(width, height);
Trace.endSection();
}
@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);
}
/**
* Set the untruncated number of children in the group so that the view can update the UI
* appropriately. Note that this may differ from the number of views attached as truncated
* children will not have views.
*/
public void setUntruncatedChildCount(int childCount) {
mUntruncatedChildCount = childCount;
updateGroupOverflow();
}
/**
* Set the notification time in the group so that the view can show the latest event in the UI
* appropriately.
*/
public void setNotificationGroupWhen(long whenMillis) {
if (mNotificationHeaderWrapper != null) {
mNotificationHeaderWrapper.setNotificationWhen(whenMillis);
}
if (mNotificationHeaderWrapperLowPriority != null) {
mNotificationHeaderWrapperLowPriority.setNotificationWhen(whenMillis);
}
}
/**
* 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) {
ensureRemovedFromTransientContainer(row);
int newIndex = childIndex < 0 ? mAttachedChildren.size() : childIndex;
mAttachedChildren.add(newIndex, row);
addView(row);
row.setUserLocked(mUserLocked);
View divider = inflateDivider();
addView(divider);
mDividers.add(newIndex, divider);
row.setContentTransformationAmount(0, false /* isLastChild */);
row.setNotificationFaded(mContainingNotificationIsFaded);
// It doesn't make sense to keep old animations around, lets cancel them!
ExpandableViewState viewState = row.getViewState();
if (viewState != null) {
viewState.cancelAnimations(row);
row.cancelAppearDrawing();
}
applyRoundnessAndInvalidate();
}
private void ensureRemovedFromTransientContainer(View v) {
if (v.getParent() != null && v instanceof ExpandableView) {
// If the child is animating away, it will still have a parent, so detach it first
// TODO: We should really cancel the active animations here. This will
// happen automatically when the view's intro animation starts, but
// it's a fragile link.
((ExpandableView) v).removeFromTransientContainerForAdditionTo(this);
}
}
public void removeNotification(ExpandableNotificationRow row) {
int childIndex = mAttachedChildren.indexOf(row);
mAttachedChildren.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.setNotificationFaded(false);
row.setUserLocked(false);
if (!row.isRemoved()) {
mGroupingUtil.restoreChildNotification(row);
}
row.requestRoundnessReset(FROM_PARENT, /* animate = */ false);
applyRoundnessAndInvalidate();
}
/**
* @return The number of notification children in the container.
*/
public int getNotificationChildCount() {
return mAttachedChildren.size();
}
public void recreateNotificationHeader(OnClickListener listener, boolean isConversation) {
mHeaderClickListener = listener;
mIsConversation = isConversation;
StatusBarNotification notification = mContainingNotification.getEntry().getSbn();
final Notification.Builder builder = Notification.Builder.recoverBuilder(getContext(),
notification.getNotification());
RemoteViews header = builder.makeNotificationGroupHeader();
if (mNotificationHeader == null) {
mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this);
mNotificationHeader.findViewById(com.android.internal.R.id.expand_button)
.setVisibility(VISIBLE);
mNotificationHeader.setOnClickListener(mHeaderClickListener);
mNotificationHeaderWrapper =
(NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
getContext(),
mNotificationHeader,
mContainingNotification);
mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
addView(mNotificationHeader, 0);
invalidate();
} else {
header.reapply(getContext(), mNotificationHeader);
}
mNotificationHeaderWrapper.setExpanded(mChildrenExpanded);
mNotificationHeaderWrapper.onContentUpdated(mContainingNotification);
recreateLowPriorityHeader(builder, isConversation);
updateHeaderVisibility(false /* animate */);
updateChildrenAppearance();
}
/**
* Recreate the low-priority header.
*
* @param builder a builder to reuse. Otherwise the builder will be recovered.
*/
private void recreateLowPriorityHeader(Notification.Builder builder, boolean isConversation) {
RemoteViews header;
StatusBarNotification notification = mContainingNotification.getEntry().getSbn();
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);
mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button)
.setVisibility(VISIBLE);
mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener);
mNotificationHeaderWrapperLowPriority =
(NotificationHeaderViewWrapper) NotificationViewWrapper.wrap(
getContext(),
mNotificationHeaderLowPriority,
mContainingNotification);
mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate);
addView(mNotificationHeaderLowPriority, 0);
invalidate();
} else {
header.reapply(getContext(), mNotificationHeaderLowPriority);
}
mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification);
resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader());
} else {
removeView(mNotificationHeaderLowPriority);
mNotificationHeaderLowPriority = null;
mNotificationHeaderWrapperLowPriority = null;
}
}
/**
* Update the appearance of the children to reduce redundancies.
*/
public void updateChildrenAppearance() {
mGroupingUtil.updateChildrenAppearance();
}
private void setExpandButtonNumber(NotificationViewWrapper wrapper) {
View expandButton = wrapper == null
? null : wrapper.getExpandButton();
if (expandButton instanceof NotificationExpandButton) {
((NotificationExpandButton) expandButton).setNumber(mUntruncatedChildCount);
}
}
public void updateGroupOverflow() {
if (mShowGroupCountInExpander) {
setExpandButtonNumber(mNotificationHeaderWrapper);
setExpandButtonNumber(mNotificationHeaderWrapperLowPriority);
return;
}
int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */);
if (mUntruncatedChildCount > maxAllowedVisibleChildren) {
int number = mUntruncatedChildCount - maxAllowedVisibleChildren;
mOverflowNumber = mHybridGroupManager.bindOverflowNumber(mOverflowNumber, number, this);
if (mGroupOverFlowState == null) {
mGroupOverFlowState = new ViewState();
mNeverAppliedGroupState = true;
}
} else if (mOverflowNumber != null) {
removeView(mOverflowNumber);
if (isShown() && isAttachedToWindow()) {
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);
}
/**
* Get notification children that are attached currently.
*/
public List<ExpandableNotificationRow> getAttachedChildren() {
return mAttachedChildren;
}
/**
* Sets the alpha on the content, while leaving the background of the container itself as is.
*
* @param alpha alpha value to apply to the content
*/
public void setContentAlpha(float alpha) {
for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
mNotificationHeader.getChildAt(i).setAlpha(alpha);
}
for (ExpandableNotificationRow child : getAttachedChildren()) {
child.setContentAlpha(alpha);
}
}
/**
* To be called any time the rows have been updated
*/
public void updateExpansionStates() {
if (mChildrenExpanded || mUserLocked) {
// we don't modify it the group is expanded or if we are expanding it
return;
}
int size = mAttachedChildren.size();
for (int i = 0; i < size; i++) {
ExpandableNotificationRow child = mAttachedChildren.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 + mCurrentHeaderTranslation;
int visibleChildren = 0;
int childCount = mAttachedChildren.size();
boolean firstChild = true;
float expandFactor = 0;
if (mUserLocked) {
expandFactor = getGroupExpandFraction();
}
boolean childrenExpanded = mChildrenExpanded;
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,
mNotificationTopPadding + mDividerHeight,
expandFactor);
} else {
intrinsicHeight += childrenExpanded
? mNotificationTopPadding + mDividerHeight
: 0;
}
firstChild = false;
}
ExpandableNotificationRow child = mAttachedChildren.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 parentState the state of the parent
*/
public void updateState(ExpandableViewState parentState) {
int childCount = mAttachedChildren.size();
int yPosition = mNotificationHeaderMargin + mCurrentHeaderTranslation;
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();
int launchTransitionCompensation = 0;
for (int i = 0; i < childCount; i++) {
ExpandableNotificationRow child = mAttachedChildren.get(i);
if (!firstChild) {
if (expandingToExpandedGroup) {
yPosition += NotificationUtils.interpolate(mChildPadding, mDividerHeight,
expandFactor);
} else {
yPosition += mChildrenExpanded ? mDividerHeight : mChildPadding;
}
} else {
if (expandingToExpandedGroup) {
yPosition += NotificationUtils.interpolate(
0,
mNotificationTopPadding + mDividerHeight,
expandFactor);
} else {
yPosition += mChildrenExpanded ? mNotificationTopPadding + mDividerHeight : 0;
}
firstChild = false;
}
ExpandableViewState childState = child.getViewState();
int intrinsicHeight = child.getIntrinsicHeight();
childState.height = intrinsicHeight;
childState.setYTranslation(yPosition + launchTransitionCompensation);
childState.hidden = false;
if (child.isExpandAnimationRunning() || mContainingNotification.hasExpandingChild()) {
// Not modifying translationZ during launch animation. The translationZ of the
// expanding child is handled inside ExpandableNotificationRow and the translationZ
// of the other children inside the group should remain unchanged. In particular,
// they should not take over the translationZ of the parent, since the parent has
// a positive translationZ set only for the expanding child to be drawn above other
// notifications.
childState.setZTranslation(child.getTranslationZ());
} else if (childrenExpandedAndNotAnimating && mEnableShadowOnChildNotifications) {
// When the group is expanded, the children cast the shadows rather than the parent
// so use the parent's elevation here.
childState.setZTranslation(parentState.getZTranslation());
} else {
childState.setZTranslation(0);
}
childState.dimmed = parentState.dimmed;
childState.hideSensitive = parentState.hideSensitive;
childState.belowSpeedBump = parentState.belowSpeedBump;
childState.clipTopAmount = 0;
childState.setAlpha(0);
if (i < firstOverflowIndex) {
childState.setAlpha(showingAsLowPriority() ? expandFactor : 1.0f);
} else if (expandFactor == 1.0f && i <= lastVisibleIndex) {
childState.setAlpha(
(mActualHeight - childState.getYTranslation()) / childState.height);
childState.setAlpha(Math.max(0.0f, Math.min(1.0f, childState.getAlpha())));
}
childState.location = parentState.location;
childState.inShelf = parentState.inShelf;
yPosition += intrinsicHeight;
}
if (mOverflowNumber != null) {
ExpandableNotificationRow overflowView = mAttachedChildren.get(Math.min(
getMaxAllowedVisibleChildren(true /* likeCollapsed */), childCount) - 1);
mGroupOverFlowState.copyFrom(overflowView.getViewState());
if (!mChildrenExpanded) {
HybridNotificationView alignView = overflowView.getSingleLineView();
if (alignView != null) {
View mirrorView = alignView.getTextView();
if (mirrorView.getVisibility() == GONE) {
mirrorView = alignView.getTitleView();
}
if (mirrorView.getVisibility() == GONE) {
mirrorView = alignView;
}
mGroupOverFlowState.setAlpha(mirrorView.getAlpha());
float yTranslation = mGroupOverFlowState.getYTranslation()
+ NotificationUtils.getRelativeYOffset(
mirrorView, overflowView);
mGroupOverFlowState.setYTranslation(yTranslation);
}
} else {
mGroupOverFlowState.setYTranslation(
mGroupOverFlowState.getYTranslation() + mNotificationHeaderMargin);
mGroupOverFlowState.setAlpha(0.0f);
}
}
if (mNotificationHeader != null) {
if (mHeaderViewState == null) {
mHeaderViewState = new ViewState();
}
mHeaderViewState.initFrom(mNotificationHeader);
if (mContainingNotification.hasExpandingChild()) {
// Not modifying translationZ during expand animation.
mHeaderViewState.setZTranslation(mNotificationHeader.getTranslationZ());
} else if (childrenExpandedAndNotAnimating) {
mHeaderViewState.setZTranslation(parentState.getZTranslation());
} else {
mHeaderViewState.setZTranslation(0);
}
mHeaderViewState.setYTranslation(mCurrentHeaderTranslation);
mHeaderViewState.setAlpha(mHeaderVisibleAmount);
// The hiding is done automatically by the alpha, otherwise we'll pick it up again
// in the next frame with the initFrom call above and have an invisible header
mHeaderViewState.hidden = false;
}
}
/**
* 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;
}
@VisibleForTesting
int getMaxAllowedVisibleChildren() {
return getMaxAllowedVisibleChildren(false /* likeCollapsed */);
}
@VisibleForTesting
int getMaxAllowedVisibleChildren(boolean likeCollapsed) {
if (!likeCollapsed && (mChildrenExpanded || mContainingNotification.isUserLocked())
&& !showingAsLowPriority()) {
return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
}
if (mIsLowPriority
|| (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded())
|| (mContainingNotification.isHeadsUpState()
&& mContainingNotification.canShowHeadsUp())) {
return NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED;
}
return NUMBER_OF_CHILDREN_WHEN_COLLAPSED;
}
/**
* Applies state to children.
*/
public void applyState() {
int childCount = mAttachedChildren.size();
ViewState tmpState = new ViewState();
float expandFraction = 0.0f;
if (mUserLocked) {
expandFraction = getGroupExpandFraction();
}
final boolean isExpanding = !showingAsLowPriority()
&& (mUserLocked || mContainingNotification.isGroupExpansionChanging());
final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded)
|| (isExpanding && !mHideDividersDuringExpand);
for (int i = 0; i < childCount; i++) {
ExpandableNotificationRow child = mAttachedChildren.get(i);
ExpandableViewState viewState = child.getViewState();
viewState.applyToView(child);
// layout the divider
View divider = mDividers.get(i);
tmpState.initFrom(divider);
tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight);
float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0;
if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) {
alpha = NotificationUtils.interpolate(0, mDividerAlpha,
Math.min(viewState.getAlpha(), expandFraction));
}
tmpState.hidden = !dividersVisible;
tmpState.setAlpha(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() {
if (mContainingNotification.hasExpandingChild()) {
return;
}
int childCount = mAttachedChildren.size();
int layoutEnd = mContainingNotification.getActualHeight() - mClipBottomAmount;
for (int i = 0; i < childCount; i++) {
ExpandableNotificationRow child = mAttachedChildren.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);
}
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
boolean isCanvasChanged = false;
Path clipPath = mChildClipPath;
if (clipPath != null) {
final float translation;
if (child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow notificationRow = (ExpandableNotificationRow) child;
translation = notificationRow.getTranslation();
} else {
translation = child.getTranslationX();
}
isCanvasChanged = true;
canvas.save();
if (translation != 0f) {
clipPath.offset(translation, 0f);
canvas.clipPath(clipPath);
clipPath.offset(-translation, 0f);
} else {
canvas.clipPath(clipPath);
}
}
if (child instanceof NotificationHeaderView
&& mNotificationHeaderWrapper.hasRoundedCorner()) {
float[] radii = mNotificationHeaderWrapper.getUpdatedRadii();
mHeaderPath.reset();
mHeaderPath.addRoundRect(
child.getLeft(),
child.getTop(),
child.getRight(),
child.getBottom(),
radii,
Direction.CW
);
if (!isCanvasChanged) {
isCanvasChanged = true;
canvas.save();
}
canvas.clipPath(mHeaderPath);
}
if (isCanvasChanged) {
boolean result = super.drawChild(canvas, child, drawingTime);
canvas.restore();
return result;
} else {
// If there have been no changes to the canvas we can proceed as usual
return super.drawChild(canvas, child, drawingTime);
}
}
/**
* This is called when the children expansion has changed and positions the children properly
* for an appear animation.
*/
public void prepareExpansionChanged() {
// TODO: do something that makes sense, like placing the invisible views correctly
return;
}
/**
* Animate to a given state.
*/
public void startAnimationToState(AnimationProperties properties) {
int childCount = mAttachedChildren.size();
ViewState tmpState = new ViewState();
float expandFraction = getGroupExpandFraction();
final boolean isExpanding = !showingAsLowPriority()
&& (mUserLocked || mContainingNotification.isGroupExpansionChanging());
final boolean dividersVisible = (mChildrenExpanded && mShowDividersWhenExpanded)
|| (isExpanding && !mHideDividersDuringExpand);
for (int i = childCount - 1; i >= 0; i--) {
ExpandableNotificationRow child = mAttachedChildren.get(i);
ExpandableViewState viewState = child.getViewState();
viewState.animateTo(child, properties);
// layout the divider
View divider = mDividers.get(i);
tmpState.initFrom(divider);
tmpState.setYTranslation(viewState.getYTranslation() - mDividerHeight);
float alpha = mChildrenExpanded && viewState.getAlpha() != 0 ? mDividerAlpha : 0;
if (mUserLocked && !showingAsLowPriority() && viewState.getAlpha() != 0) {
alpha = NotificationUtils.interpolate(0, mDividerAlpha,
Math.min(viewState.getAlpha(), expandFraction));
}
tmpState.hidden = !dividersVisible;
tmpState.setAlpha(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.getAlpha();
mGroupOverFlowState.setAlpha(0);
mGroupOverFlowState.applyToView(mOverflowNumber);
mGroupOverFlowState.setAlpha(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 = mAttachedChildren.size();
for (int childIdx = 0; childIdx < count; childIdx++) {
ExpandableNotificationRow slidingChild = mAttachedChildren.get(childIdx);
float childTop = slidingChild.getTranslationY();
float top = childTop + Math.max(0, 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 (mNotificationHeaderWrapper != null) {
mNotificationHeaderWrapper.setExpanded(childrenExpanded);
}
final int count = mAttachedChildren.size();
for (int childIdx = 0; childIdx < count; childIdx++) {
ExpandableNotificationRow child = mAttachedChildren.get(childIdx);
child.setChildrenExpanded(childrenExpanded, false);
}
updateHeaderTouchability();
}
public void setContainingNotification(ExpandableNotificationRow parent) {
mContainingNotification = parent;
mGroupingUtil = new NotificationGroupingUtil(mContainingNotification);
}
public ExpandableNotificationRow getContainingNotification() {
return mContainingNotification;
}
public NotificationViewWrapper getNotificationViewWrapper() {
return mNotificationHeaderWrapper;
}
public NotificationViewWrapper getLowPriorityViewWrapper() {
return mNotificationHeaderWrapperLowPriority;
}
@VisibleForTesting
public ViewGroup getCurrentHeaderView() {
return mCurrentHeader;
}
private void updateHeaderVisibility(boolean animate) {
ViewGroup desiredHeader;
ViewGroup currentHeader = mCurrentHeader;
desiredHeader = calculateDesiredHeader();
if (currentHeader == desiredHeader) {
return;
}
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(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 (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 = mAttachedChildren.size();
for (int i = 0; i < childCount; i++) {
if (i >= NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED) {
break;
}
ExpandableNotificationRow child = mAttachedChildren.get(i);
child.setAlpha(start);
ViewState viewState = new ViewState();
viewState.initFrom(child);
viewState.setAlpha(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;
}
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 + mCurrentHeaderTranslation
+ mNotificationTopPadding;
int visibleChildren = 0;
int childCount = mAttachedChildren.size();
for (int i = 0; i < childCount; i++) {
if (visibleChildren >= NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED) {
break;
}
ExpandableNotificationRow child = mAttachedChildren.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 = mAttachedChildren.size();
for (int i = 0; i < childCount; i++) {
ExpandableNotificationRow child = mAttachedChildren.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 + mCurrentHeaderTranslation
+ mNotificationTopPadding + mDividerHeight;
int visibleChildren = 0;
int childCount = mAttachedChildren.size();
int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* forceCollapsed */);
for (int i = 0; i < childCount; i++) {
if (visibleChildren >= maxAllowedVisibleChildren) {
break;
}
ExpandableNotificationRow child = mAttachedChildren.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, false /* likeHighPriority */);
}
public int getCollapsedHeight() {
return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */),
false /* likeHighPriority */);
}
public int getCollapsedHeightWithoutHeader() {
return getMinHeight(getMaxAllowedVisibleChildren(true /* forceCollapsed */),
false /* likeHighPriority */, 0);
}
/**
* 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) {
return getMinHeight(maxAllowedVisibleChildren, likeHighPriority, mCurrentHeaderTranslation);
}
/**
* 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
* @param headerTranslation the translation amount of the header
*/
private int getMinHeight(
int maxAllowedVisibleChildren,
boolean likeHighPriority,
int headerTranslation) {
if (!likeHighPriority && showingAsLowPriority()) {
if (mNotificationHeaderLowPriority == null) {
Log.e(TAG, "getMinHeight: low priority header is null", new Exception());
return 0;
}
return mNotificationHeaderLowPriority.getHeight();
}
int minExpandHeight = mNotificationHeaderMargin + headerTranslation;
int visibleChildren = 0;
boolean firstChild = true;
int childCount = mAttachedChildren.size();
for (int i = 0; i < childCount; i++) {
if (visibleChildren >= maxAllowedVisibleChildren) {
break;
}
if (!firstChild) {
minExpandHeight += mChildPadding;
} else {
firstChild = false;
}
ExpandableNotificationRow child = mAttachedChildren.get(i);
View singleLineView = child.getSingleLineView();
if (singleLineView != null) {
minExpandHeight += singleLineView.getHeight();
} else {
Log.e(TAG, "getMinHeight: child " + child + " single line view is null",
new Exception());
}
visibleChildren++;
}
minExpandHeight += mCollapsedBottomPadding;
return minExpandHeight;
}
public boolean showingAsLowPriority() {
return mIsLowPriority && !mContainingNotification.isExpanded();
}
public void reInflateViews(OnClickListener listener, StatusBarNotification notification) {
if (mNotificationHeader != null) {
removeView(mNotificationHeader);
mNotificationHeader = null;
}
if (mNotificationHeaderLowPriority != null) {
removeView(mNotificationHeaderLowPriority);
mNotificationHeaderLowPriority = null;
}
recreateNotificationHeader(listener, mIsConversation);
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 = mAttachedChildren.size();
for (int i = 0; i < childCount; i++) {
ExpandableNotificationRow child = mAttachedChildren.get(i);
child.setUserLocked(userLocked && !showingAsLowPriority());
}
updateHeaderTouchability();
}
private void updateHeaderTouchability() {
if (mNotificationHeader != null) {
mNotificationHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked);
}
}
public void onNotificationUpdated() {
if (mShowGroupCountInExpander) {
// The overflow number is not used, so its color is irrelevant; skip this
return;
}
int color = mContainingNotification.getNotificationColor();
Resources.Theme theme = new ContextThemeWrapper(mContext,
com.android.internal.R.style.Theme_DeviceDefault_DayNight).getTheme();
try (TypedArray ta = theme.obtainStyledAttributes(
new int[]{com.android.internal.R.attr.colorAccent})) {
color = ta.getColor(0, color);
}
mHybridGroupManager.setOverflowNumberColor(mOverflowNumber, color);
}
public int getPositionInLinearLayout(View childInGroup) {
int position = mNotificationHeaderMargin + mCurrentHeaderTranslation
+ mNotificationTopPadding;
for (int i = 0; i < mAttachedChildren.size(); i++) {
ExpandableNotificationRow child = mAttachedChildren.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 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 */, mIsConversation);
updateHeaderVisibility(false /* animate */);
}
if (mUserLocked) {
setUserLocked(mUserLocked);
}
}
/**
* @return the view wrapper for the currently showing priority.
*/
public NotificationViewWrapper getVisibleWrapper() {
if (showingAsLowPriority()) {
return mNotificationHeaderWrapperLowPriority;
}
return mNotificationHeaderWrapper;
}
public void onExpansionChanged() {
if (mIsLowPriority) {
if (mUserLocked) {
setUserLocked(mUserLocked);
}
updateHeaderVisibility(true /* animate */);
}
}
@VisibleForTesting
public boolean isUserLocked() {
return mUserLocked;
}
@Override
public void applyRoundnessAndInvalidate() {
boolean last = true;
if (mNotificationHeaderWrapper != null) {
mNotificationHeaderWrapper.requestTopRoundness(
/* value = */ getTopRoundness(),
/* sourceType = */ FROM_PARENT,
/* animate = */ false
);
}
if (mNotificationHeaderWrapperLowPriority != null) {
mNotificationHeaderWrapperLowPriority.requestTopRoundness(
/* value = */ getTopRoundness(),
/* sourceType = */ FROM_PARENT,
/* animate = */ false
);
}
for (int i = mAttachedChildren.size() - 1; i >= 0; i--) {
ExpandableNotificationRow child = mAttachedChildren.get(i);
if (child.getVisibility() == View.GONE) {
continue;
}
child.requestRoundness(
/* top = */ 0f,
/* bottom = */ last ? getBottomRoundness() : 0f,
/* sourceType = */ FROM_PARENT,
/* animate = */ false);
last = false;
}
Roundable.super.applyRoundnessAndInvalidate();
}
public void setHeaderVisibleAmount(float headerVisibleAmount) {
mHeaderVisibleAmount = headerVisibleAmount;
mCurrentHeaderTranslation = (int) ((1.0f - headerVisibleAmount) * mTranslationForHeader);
}
/**
* Shows the given feedback icon, or hides the icon if null.
*/
public void setFeedbackIcon(@Nullable FeedbackIcon icon) {
if (mNotificationHeaderWrapper != null) {
mNotificationHeaderWrapper.setFeedbackIcon(icon);
}
if (mNotificationHeaderWrapperLowPriority != null) {
mNotificationHeaderWrapperLowPriority.setFeedbackIcon(icon);
}
}
public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) {
if (mNotificationHeaderWrapper != null) {
mNotificationHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
}
if (mNotificationHeaderWrapperLowPriority != null) {
mNotificationHeaderWrapperLowPriority.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
}
}
@Override
public void setNotificationFaded(boolean faded) {
mContainingNotificationIsFaded = faded;
if (mNotificationHeaderWrapper != null) {
mNotificationHeaderWrapper.setNotificationFaded(faded);
}
if (mNotificationHeaderWrapperLowPriority != null) {
mNotificationHeaderWrapperLowPriority.setNotificationFaded(faded);
}
for (ExpandableNotificationRow child : mAttachedChildren) {
child.setNotificationFaded(faded);
}
}
/**
* Allow to define a path the clip the children in #drawChild()
*
* @param childClipPath path used to clip the children
*/
public void setChildClipPath(@Nullable Path childClipPath) {
mChildClipPath = childClipPath;
invalidate();
}
public NotificationHeaderViewWrapper getNotificationHeaderWrapper() {
return mNotificationHeaderWrapper;
}
public String debugString() {
return TAG + " { "
+ "visibility: " + getVisibility()
+ ", alpha: " + getAlpha()
+ ", translationY: " + getTranslationY()
+ ", roundableState: " + getRoundableState().debugString() + "}";
}
}