android:layout_height="wrap_content"
android:paddingStart="@*android:dimen/notification_content_margin_start"
android:paddingEnd="12dp"
- android:gravity="bottom">
+ android:gravity="bottom|start">
<TextView
android:id="@+id/notification_title"
android:layout_width="wrap_content"
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 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
+ -->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/notification_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@*android:style/TextAppearance.Material.Notification"
+ android:paddingEnd="@*android:dimen/notification_content_margin_end"
+ android:gravity="end"
+ android:singleLine="true"
+ />
\ No newline at end of file
<!-- Content description of the clear button in the notification panel for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="accessibility_clear_all">Clear all notifications.</string>
+ <!-- The overflow indicator shown when a group has more notification inside the group than the visible ones. An example is "+ 3" [CHAR LIMIT=5] -->
+ <string name="notification_group_overflow_indicator">+ <xliff:g id="number" example="3">%s</xliff:g></string>
+
<!-- Content description of button in notification inspector for system settings relating to
notifications from this application [CHAR LIMIT=NONE] -->
<string name="status_bar_notification_inspect_item_title">Notification settings</string>
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingManager;
+import com.android.systemui.statusbar.notification.HybridNotificationView;
import com.android.systemui.statusbar.notification.NotificationViewWrapper;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.HeadsUpManager;
updateClearability();
if (mIsSummaryWithChildren) {
recreateNotificationHeader();
+ mChildrenContainer.onNotificationUpdated();
}
if (mIconAnimationRunning) {
setIconAnimationRunning(true);
mPublicLayout.closeRemoteInput();
}
+ /**
+ * Set by how much the single line view should be indented.
+ */
+ public void setSingleLineWidthIndention(int indention) {
+ mPrivateLayout.setSingleLineWidthIndention(indention);
+ }
+
+ public int getNotificationColor() {
+ int color = getStatusBarNotification().getNotification().color;
+ if (color == Notification.COLOR_DEFAULT) {
+ return mContext.getColor(com.android.internal.R.color.notification_icon_default_color);
+ }
+ return color;
+ }
+
+ public HybridNotificationView getSingleLineView() {
+ return mPrivateLayout.getSingleLineView();
+ }
+
public interface ExpansionLogger {
public void logNotificationExpansion(String key, boolean userAction, boolean expanded);
}
public void onInflate(ViewStub stub, View inflated) {
mChildrenContainer = (NotificationChildrenContainer) inflated;
mChildrenContainer.setNotificationParent(ExpandableNotificationRow.this);
+ mChildrenContainer.onNotificationUpdated();
mTranslateableViews.add(mChildrenContainer);
}
});
showing.setDark(dark, fade, delay);
}
if (mIsSummaryWithChildren) {
+ mChildrenContainer.setDark(dark, fade, delay);
mNotificationHeaderWrapper.setDark(dark, fade, delay);
}
}
mIsSystemExpanded = expand;
notifyHeightChanged(false /* needsAnimation */);
logExpansionEvent(false, wasExpanded);
+ if (mChildrenContainer != null) {
+ mChildrenContainer.updateGroupOverflow();
+ }
}
}
mOnKeyguard = onKeyguard;
logExpansionEvent(false, wasExpanded);
if (wasExpanded != isExpanded()) {
+ if (mIsSummaryWithChildren) {
+ mChildrenContainer.updateGroupOverflow();
+ }
notifyHeightChanged(false /* needsAnimation */);
}
}
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.HybridNotificationView;
-import com.android.systemui.statusbar.notification.HybridNotificationViewManager;
+import com.android.systemui.statusbar.notification.HybridGroupManager;
import com.android.systemui.statusbar.notification.NotificationCustomViewWrapper;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.NotificationViewWrapper;
private NotificationViewWrapper mContractedWrapper;
private NotificationViewWrapper mExpandedWrapper;
private NotificationViewWrapper mHeadsUpWrapper;
- private HybridNotificationViewManager mHybridViewManager;
+ private HybridGroupManager mHybridGroupManager;
private int mClipTopAmount;
private int mContentHeight;
private int mUnrestrictedContentHeight;
private ExpandableNotificationRow mContainingNotification;
private int mTransformationStartVisibleType;
private boolean mUserExpanding;
+ private int mSingleLineWidthIndention;
public NotificationContentView(Context context, AttributeSet attrs) {
super(context, attrs);
- mHybridViewManager = new HybridNotificationViewManager(getContext(), this);
+ mHybridGroupManager = new HybridGroupManager(getContext(), this);
mMinContractedHeight = getResources().getDimensionPixelSize(
R.dimen.min_notification_layout_height);
mNotificationContentMarginEnd = getResources().getDimensionPixelSize(
boolean hasFixedHeight = heightMode == MeasureSpec.EXACTLY;
boolean isHeightLimited = heightMode == MeasureSpec.AT_MOST;
int maxSize = Integer.MAX_VALUE;
+ int width = MeasureSpec.getSize(widthMeasureSpec);
if (hasFixedHeight || isHeightLimited) {
maxSize = MeasureSpec.getSize(heightMeasureSpec);
}
maxChildHeight = Math.max(maxChildHeight, mHeadsUpChild.getMeasuredHeight());
}
if (mSingleLineView != null) {
- mSingleLineView.measure(widthMeasureSpec,
+ int singleLineWidthSpec = widthMeasureSpec;
+ if (mSingleLineWidthIndention != 0
+ && MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
+ singleLineWidthSpec = MeasureSpec.makeMeasureSpec(
+ width - mSingleLineWidthIndention + mSingleLineView.getPaddingEnd(),
+ MeasureSpec.AT_MOST);
+ }
+ mSingleLineView.measure(singleLineWidthSpec,
MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.AT_MOST));
maxChildHeight = Math.max(maxChildHeight, mSingleLineView.getMeasuredHeight());
}
int ownHeight = Math.min(maxChildHeight, maxSize);
- int width = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(width, ownHeight);
}
private void updateSingleLineView() {
if (mIsChildInGroup) {
- mSingleLineView = mHybridViewManager.bindFromNotification(
+ mSingleLineView = mHybridGroupManager.bindFromNotification(
mSingleLineView, mStatusBarNotification.getNotification());
} else if (mSingleLineView != null) {
removeView(mSingleLineView);
updateBackgroundColor(false);
}
}
+
+ /**
+ * Set by how much the single line view should be indented. Used when a overflow indicator is
+ * present and only during measuring
+ */
+ public void setSingleLineWidthIndention(int singleLineWidthIndention) {
+ if (singleLineWidthIndention != mSingleLineWidthIndention) {
+ mSingleLineWidthIndention = singleLineWidthIndention;
+ mContainingNotification.forceLayout();
+ forceLayout();
+ }
+ }
+
+ public HybridNotificationView getSingleLineView() {
+ return mSingleLineView;
+ }
}
import android.app.Notification;
import android.content.Context;
+import android.text.BidiFormatter;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
+import android.widget.TextView;
import com.android.systemui.R;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+import java.util.List;
/**
- * A class managing {@link HybridNotificationView} views
+ * A class managing hybrid groups that include {@link HybridNotificationView} and the notification
+ * group overflow.
*/
-public class HybridNotificationViewManager {
+public class HybridGroupManager {
private final Context mContext;
private ViewGroup mParent;
- private String mDivider;
+ private int mOverflowNumberColor;
- public HybridNotificationViewManager(Context ctx, ViewGroup parent) {
+ public HybridGroupManager(Context ctx, ViewGroup parent) {
mContext = ctx;
mParent = parent;
- mDivider = " • ";
}
private HybridNotificationView inflateHybridView() {
return hybrid;
}
+ private TextView inflateOverflowNumber() {
+ LayoutInflater inflater = mContext.getSystemService(LayoutInflater.class);
+ TextView numberView = (TextView) inflater.inflate(
+ R.layout.hybrid_overflow_number, mParent, false);
+ mParent.addView(numberView);
+ updateOverFlowNumberColor(numberView);
+ return numberView;
+ }
+
+ private void updateOverFlowNumberColor(TextView numberView) {
+ numberView.setTextColor(mOverflowNumberColor);
+ }
+
+ public void setOverflowNumberColor(TextView numberView, int overflowNumberColor) {
+ mOverflowNumberColor = overflowNumberColor;
+ if (numberView != null) {
+ updateOverFlowNumberColor(numberView);
+ }
+ }
+
public HybridNotificationView bindFromNotification(HybridNotificationView reusableView,
Notification notification) {
if (reusableView == null) {
}
return titleText;
}
+
+ public TextView bindOverflowNumber(TextView reusableView, int number) {
+ if (reusableView == null) {
+ reusableView = inflateOverflowNumber();
+ }
+ String text = mContext.getResources().getString(
+ R.string.notification_group_overflow_indicator, number);
+ if (!text.equals(reusableView.getText())) {
+ reusableView.setText(text);
+ }
+ return reusableView;
+ }
}
super(context, attrs, defStyleAttr, defStyleRes);
}
+ public TextView getTitleView() {
+ return mTitleView;
+ }
+
+ public TextView getTextView() {
+ return mTextView;
+ }
+
@Override
protected void onFinishInflate() {
super.onFinishInflate();
package com.android.systemui.statusbar.notification;
import android.graphics.Color;
+import android.view.View;
import android.widget.ImageView;
import com.android.internal.util.NotificationColorUtil;
import com.android.systemui.R;
+import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
/**
* A util class for various reusable functions
*/
public class NotificationUtils {
+ private static final int[] sLocationBase = new int[2];
+ private static final int[] sLocationOffset = new int[2];
public static boolean isGrayscale(ImageView v, NotificationColorUtil colorUtil) {
Object isGrayscale = v.getTag(R.id.icon_is_grayscale);
if (isGrayscale != null) {
(int) interpolate(Color.green(startColor), Color.green(endColor), amount),
(int) interpolate(Color.blue(startColor), Color.blue(endColor), amount));
}
+
+ public static float getRelativeYOffset(View offsetView, View baseView) {
+ baseView.getLocationOnScreen(sLocationBase);
+ offsetView.getLocationOnScreen(sLocationOffset);
+ return sLocationOffset[1] - sLocationBase[1];
+ }
}
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+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.notification.HybridGroupManager;
import com.android.systemui.statusbar.notification.HybridNotificationView;
-import com.android.systemui.statusbar.notification.HybridNotificationViewManager;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.phone.NotificationPanelView;
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 mNotificationHeaderHeight;
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 int mLayoutDirection = LAYOUT_DIRECTION_UNDEFINED;
private boolean mUserLocked;
private int mActualHeight;
+ private boolean mNeverAppliedGroupState;
public NotificationChildrenContainer(Context context) {
this(context, null);
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initDimens();
+ mHybridGroupManager = new HybridGroupManager(getContext(), this);
}
private void initDimens() {
if (child.getVisibility() == View.GONE) {
continue;
}
- child.layout(0, 0, getWidth(), child.getMeasuredHeight());
+ child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
mDividers.get(i).layout(0, 0, getWidth(), mDividerHeight);
}
+ if (mOverflowNumber != null) {
+ mOverflowNumber.layout(getWidth() - mOverflowNumber.getMeasuredWidth(), 0, getWidth(),
+ mOverflowNumber.getMeasuredHeight());
+ }
}
@Override
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 = mNotificationHeaderHeight + 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++) {
- View child = mChildren.get(i);
+ ExpandableNotificationRow child = mChildren.get(i);
+ boolean isOverflow = i == overflowIndex;
+ child.setSingleLineWidthIndention(isOverflow ? mOverflowNumber.getMeasuredWidth() : 0);
child.measure(widthMeasureSpec, newHeightSpec);
height += child.getMeasuredHeight();
divider.measure(widthMeasureSpec, dividerHeightSpec);
height += mDividerHeight;
}
- int width = MeasureSpec.getSize(widthMeasureSpec);
mRealHeight = height;
if (heightMode != MeasureSpec.UNSPECIFIED) {
height = Math.min(height, size);
View divider = inflateDivider();
addView(divider);
mDividers.add(newIndex, divider);
+
+ updateGroupOverflow();
}
public void removeNotification(ExpandableNotificationRow row) {
row.setSystemChildExpanded(false);
row.setUserLocked(false);
+ updateGroupOverflow();
+ }
+
+ 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);
- int layoutDirection = getLayoutDirection();
- if (layoutDirection != mLayoutDirection) {
- mLayoutDirection = layoutDirection;
- }
+ updateGroupOverflow();
}
private View inflateDivider() {
int yPosition = mNotificationHeaderHeight;
boolean firstChild = true;
int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren();
+ boolean hasOverflow = !mChildrenExpanded && childCount > maxAllowedVisibleChildren
+ && maxAllowedVisibleChildren != NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED;
int lastVisibleIndex = maxAllowedVisibleChildren - 1;
float expandFactor = 0;
if (mUserLocked) {
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 += mNotificationHeaderHeight;
+ mGroupOverFlowState.alpha = 0.0f;
+ }
+ }
}
private int getMaxAllowedVisibleChildren() {
public void applyState(StackScrollState state) {
int childCount = mChildren.size();
ViewState tmpState = new ViewState();
- float expandFraction = getChildExpandFraction();
+ float expandFraction = 0.0f;
+ if (mUserLocked) {
+ expandFraction = getChildExpandFraction();
+ }
for (int i = 0; i < childCount; i++) {
ExpandableNotificationRow child = mChildren.get(i);
StackViewState viewState = state.getViewStateForView(child);
// 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;
+ }
}
/**
// 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);
+ }
}
public ExpandableNotificationRow getViewAtPosition(float y) {
return minExpandHeight;
}
+ public void setDark(boolean dark, boolean fade, long delay) {
+ if (mOverflowNumber != null) {
+ mOverflowInvertHelper.setInverted(dark, fade, delay);
+ }
+ }
+
public void reInflateViews() {
initDimens();
for (int i = 0; i < mDividers.size(); i++) {
child.setUserLocked(userLocked);
}
}
+
+ public void onNotificationUpdated() {
+ mHybridGroupManager.setOverflowNumberColor(mOverflowNumber,
+ mNotificationParent.getNotificationColor());
+ }
}
}
/**
- * Get the end value of the height animation running on a view or the actualHeight
+ * Get the end value of the yTranslation animation running on a view or the yTranslation
* if no animation is running.
*/
- public static float getFinalTranslationY(ExpandableView view) {
+ public static float getFinalTranslationY(View view) {
if (view == null) {
return 0;
}