diff options
| -rw-r--r-- | core/java/com/android/internal/widget/ConversationHeaderLinearLayout.java | 179 | ||||
| -rw-r--r-- | core/res/res/layout/notification_template_conversation_header.xml | 5 |
2 files changed, 182 insertions, 2 deletions
diff --git a/core/java/com/android/internal/widget/ConversationHeaderLinearLayout.java b/core/java/com/android/internal/widget/ConversationHeaderLinearLayout.java new file mode 100644 index 000000000000..481183e700a2 --- /dev/null +++ b/core/java/com/android/internal/widget/ConversationHeaderLinearLayout.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2021 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.annotation.Nullable; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RemoteViews; + +import java.util.LinkedList; +import java.util.List; + +/** + * This is a subclass of LinearLayout meant to be used in the Conversation header, to fix a bug + * when multiple user-provided strings are shown in the same conversation header. b/189723284 + * + * This works around a deficiency in LinearLayout when shrinking views that it can't fully reduce + * all contents if any of the oversized views reaches zero. + */ +@RemoteViews.RemoteView +public class ConversationHeaderLinearLayout extends LinearLayout { + + public ConversationHeaderLinearLayout(Context context) { + super(context); + } + + public ConversationHeaderLinearLayout(Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public ConversationHeaderLinearLayout(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + private int calculateTotalChildLength() { + final int count = getChildCount(); + int totalLength = 0; + + for (int i = 0; i < count; ++i) { + final View child = getChildAt(i); + if (child == null || child.getVisibility() == GONE) { + continue; + } + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + child.getLayoutParams(); + totalLength += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } + return totalLength + getPaddingLeft() + getPaddingRight(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + final int containerWidth = getMeasuredWidth(); + final int contentsWidth = calculateTotalChildLength(); + + int excessContents = contentsWidth - containerWidth; + if (excessContents <= 0) { + return; + } + final int count = getChildCount(); + + float remainingWeight = 0; + List<ViewInfo> visibleChildrenToShorten = null; + + // Find children which need to be shortened in order to ensure the contents fit. + for (int i = 0; i < count; ++i) { + final View child = getChildAt(i); + if (child == null || child.getVisibility() == View.GONE) { + continue; + } + final float weight = ((LayoutParams) child.getLayoutParams()).weight; + if (weight == 0) { + continue; + } + if (child.getMeasuredWidth() == 0) { + continue; + } + if (visibleChildrenToShorten == null) { + visibleChildrenToShorten = new LinkedList<>(); + } + visibleChildrenToShorten.add(new ViewInfo(child)); + remainingWeight += Math.max(0, weight); + } + if (visibleChildrenToShorten == null || visibleChildrenToShorten.isEmpty()) { + return; + } + balanceViewWidths(visibleChildrenToShorten, remainingWeight, excessContents); + remeasureChangedChildren(visibleChildrenToShorten); + } + + /** + * Measure any child with a width that has changed. + */ + private void remeasureChangedChildren(List<ViewInfo> childrenInfo) { + for (ViewInfo info : childrenInfo) { + if (info.mWidth != info.mStartWidth) { + final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.max(0, info.mWidth), MeasureSpec.EXACTLY); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + info.mView.getMeasuredHeight(), MeasureSpec.EXACTLY); + info.mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } + } + + /** + * Given a list of view, use the weights to remove width from each view proportionally to the + * weight (and ignoring the view's actual width), but do this iteratively whenever a view is + * reduced to zero width, because in that case other views need reduction. + */ + void balanceViewWidths(List<ViewInfo> viewInfos, float weightSum, int excessContents) { + boolean performAnotherPass = true; + // Loops only when all of the following are true: + // * `performAnotherPass` -- a view clamped to 0 width (or the first iteration) + // * `excessContents > 0` -- there is still horizontal space to allocate + // * `weightSum > 0` -- at least 1 view with nonzero width AND nonzero weight left + while (performAnotherPass && excessContents > 0 && weightSum > 0) { + int excessRemovedDuringThisPass = 0; + float weightSumForNextPass = 0; + performAnotherPass = false; + for (ViewInfo info : viewInfos) { + if (info.mWeight <= 0) { + continue; + } + if (info.mWidth <= 0) { + continue; + } + int newWidth = (int) (info.mWidth - (excessContents * (info.mWeight / weightSum))); + if (newWidth < 0) { + newWidth = 0; + performAnotherPass = true; + } + excessRemovedDuringThisPass += info.mWidth - newWidth; + info.mWidth = newWidth; + if (info.mWidth > 0) { + weightSumForNextPass += info.mWeight; + } + } + excessContents -= excessRemovedDuringThisPass; + weightSum = weightSumForNextPass; + } + } + + /** + * A helper class for measuring children. + */ + static class ViewInfo { + final View mView; + final float mWeight; + final int mStartWidth; + int mWidth; + + ViewInfo(View view) { + this.mView = view; + this.mWeight = ((LayoutParams) view.getLayoutParams()).weight; + this.mStartWidth = this.mWidth = view.getMeasuredWidth(); + } + } +} diff --git a/core/res/res/layout/notification_template_conversation_header.xml b/core/res/res/layout/notification_template_conversation_header.xml index 389637eb2517..2faff412565e 100644 --- a/core/res/res/layout/notification_template_conversation_header.xml +++ b/core/res/res/layout/notification_template_conversation_header.xml @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<LinearLayout +<com.android.internal.widget.ConversationHeaderLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/conversation_header" android:layout_width="wrap_content" @@ -119,6 +119,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" + android:layout_weight="100" android:showRelative="true" android:singleLine="true" android:visibility="gone" @@ -171,4 +172,4 @@ android:src="@drawable/ic_notifications_alerted" android:visibility="gone" /> -</LinearLayout> +</com.android.internal.widget.ConversationHeaderLinearLayout> |