diff options
3 files changed, 270 insertions, 6 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 0c2e3c189aae..460640e104a5 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -3412,10 +3412,6 @@ public class Notification implements Parcelable validRemoteInput |= hasValidRemoteInput(action); final RemoteViews button = generateActionButton(action); - if (i == N - 1) { - button.setViewLayoutWidth(com.android.internal.R.id.action0, - ViewGroup.LayoutParams.MATCH_PARENT); - } big.addView(R.id.actions, button); } } else { diff --git a/core/java/com/android/internal/widget/NotificationActionListLayout.java b/core/java/com/android/internal/widget/NotificationActionListLayout.java new file mode 100644 index 000000000000..9dd118c5a942 --- /dev/null +++ b/core/java/com/android/internal/widget/NotificationActionListLayout.java @@ -0,0 +1,268 @@ +/* + * 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 + */ + +package com.android.internal.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RemoteViews; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Comparator; + +/** + * Layout for notification actions that ensures that no action consumes more than their share of + * the remaining available width, and the last action consumes the remaining space. + */ +@RemoteViews.RemoteView +public class NotificationActionListLayout extends ViewGroup { + + private int mTotalWidth = 0; + private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>(); + private ArrayList<View> mMeasureOrderOther = new ArrayList<>(); + + public NotificationActionListLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int N = getChildCount(); + int textViews = 0; + int otherViews = 0; + int notGoneChildren = 0; + + View lastNotGoneChild = null; + for (int i = 0; i < N; i++) { + View c = getChildAt(i); + if (c instanceof TextView) { + textViews++; + } else { + otherViews++; + } + if (c.getVisibility() != GONE) { + notGoneChildren++; + lastNotGoneChild = c; + } + } + + // Rebuild the measure order if the number of children changed or the text length of + // any of the children changed. + boolean needRebuild = false; + if (textViews != mMeasureOrderTextViews.size() + || otherViews != mMeasureOrderOther.size()) { + needRebuild = true; + } + if (!needRebuild) { + final int size = mMeasureOrderTextViews.size(); + for (int i = 0; i < size; i++) { + Pair<Integer, TextView> pair = mMeasureOrderTextViews.get(i); + if (pair.first != pair.second.getText().length()) { + needRebuild = true; + } + } + } + if (notGoneChildren > 1 && needRebuild) { + rebuildMeasureOrder(textViews, otherViews); + } + + final boolean constrained = + MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED; + + final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight; + final int otherSize = mMeasureOrderOther.size(); + int usedWidth = 0; + + // Optimization: Don't do this if there's only one child. + int measuredChildren = 0; + for (int i = 0; i < N && notGoneChildren > 1; i++) { + // Measure shortest children first. To avoid measuring twice, we approximate by looking + // at the text length. + View c; + if (i < otherSize) { + c = mMeasureOrderOther.get(i); + } else { + c = mMeasureOrderTextViews.get(i - otherSize).second; + } + if (c.getVisibility() == GONE) { + continue; + } + MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams(); + + int usedWidthForChild = usedWidth; + if (constrained) { + // Make sure that this child doesn't consume more than its share of the remaining + // total available space. Not used space will benefit subsequent views. Since we + // measure in the order of (approx.) size, a large view can still take more than its + // share if the others are small. + int availableWidth = innerWidth - usedWidth; + int maxWidthForChild = availableWidth / (notGoneChildren - measuredChildren); + + usedWidthForChild = innerWidth - maxWidthForChild; + } + + measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild, + heightMeasureSpec, 0 /* usedHeight */); + + usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin; + measuredChildren++; + } + + // Make sure to measure the last child full-width if we didn't use up the entire width, + // or we didn't measure yet because there's just one child. + if (lastNotGoneChild != null && (constrained && usedWidth < innerWidth + || notGoneChildren == 1)) { + MarginLayoutParams lp = (MarginLayoutParams) lastNotGoneChild.getLayoutParams(); + if (notGoneChildren > 1) { + // Need to make room, since we already measured this once. + usedWidth -= lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin; + } + + int originalWidth = lp.width; + lp.width = LayoutParams.MATCH_PARENT; + measureChildWithMargins(lastNotGoneChild, widthMeasureSpec, usedWidth, + heightMeasureSpec, 0 /* usedHeight */); + lp.width = originalWidth; + + usedWidth += lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin; + } + + mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft; + setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec), + resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec)); + } + + private void rebuildMeasureOrder(int capacityText, int capacityOther) { + clearMeasureOrder(); + mMeasureOrderTextViews.ensureCapacity(capacityText); + mMeasureOrderOther.ensureCapacity(capacityOther); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View c = getChildAt(i); + if (c instanceof TextView && ((TextView) c).getText().length() > 0) { + mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(), + (TextView)c)); + } else { + mMeasureOrderOther.add(c); + } + } + mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR); + } + + private void clearMeasureOrder() { + mMeasureOrderOther.clear(); + mMeasureOrderTextViews.clear(); + } + + @Override + public void onViewAdded(View child) { + super.onViewAdded(child); + clearMeasureOrder(); + } + + @Override + public void onViewRemoved(View child) { + super.onViewRemoved(child); + clearMeasureOrder(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final boolean isLayoutRtl = isLayoutRtl(); + final int paddingTop = mPaddingTop; + + int childTop; + int childLeft; + + // Where bottom of child should go + final int height = bottom - top; + + // Space available for child + int innerHeight = height - paddingTop - mPaddingBottom; + + final int count = getChildCount(); + + final int layoutDirection = getLayoutDirection(); + switch (Gravity.getAbsoluteGravity(Gravity.START, layoutDirection)) { + case Gravity.RIGHT: + // mTotalWidth contains the padding already + childLeft = mPaddingLeft + right - left - mTotalWidth; + break; + + case Gravity.LEFT: + default: + childLeft = mPaddingLeft; + break; + } + + int start = 0; + int dir = 1; + //In case of RTL, start drawing from the last child. + if (isLayoutRtl) { + start = count - 1; + dir = -1; + } + + for (int i = 0; i < count; i++) { + final int childIndex = start + dir * i; + final View child = getChildAt(childIndex); + if (child.getVisibility() != GONE) { + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + childTop = paddingTop + ((innerHeight - childHeight) / 2) + + lp.topMargin - lp.bottomMargin; + + childLeft += lp.leftMargin; + child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); + childLeft += childWidth + lp.rightMargin; + } + } + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new MarginLayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + } + + @Override + protected LayoutParams generateLayoutParams(LayoutParams p) { + if (p instanceof MarginLayoutParams) { + return new MarginLayoutParams((MarginLayoutParams)p); + } + return new MarginLayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(LayoutParams p) { + return p instanceof MarginLayoutParams; + } + + public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR + = (a, b) -> a.first.compareTo(b.first); +} diff --git a/core/res/res/layout/notification_material_action_list.xml b/core/res/res/layout/notification_material_action_list.xml index ac37c4a4a8e2..547d3cafb621 100644 --- a/core/res/res/layout/notification_material_action_list.xml +++ b/core/res/res/layout/notification_material_action_list.xml @@ -18,7 +18,7 @@ android:id="@+id/actions_container" android:layout_width="match_parent" android:layout_height="wrap_content"> - <LinearLayout + <com.android.internal.widget.NotificationActionListLayout android:id="@+id/actions" android:layout_width="match_parent" android:layout_height="56dp" @@ -29,5 +29,5 @@ android:background="@color/notification_action_list" > <!-- actions will be added here --> - </LinearLayout> + </com.android.internal.widget.NotificationActionListLayout> </FrameLayout> |