Loading core/java/com/android/internal/widget/ConversationHeaderLinearLayout.java 0 → 100644 +179 −0 Original line number Original line Diff line number Diff line /* * 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(); } } } core/res/res/layout/notification_template_conversation_header.xml +3 −2 Original line number Original line Diff line number Diff line Loading @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ See the License for the specific language governing permissions and ~ limitations under the License ~ limitations under the License --> --> <LinearLayout <com.android.internal.widget.ConversationHeaderLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/conversation_header" android:id="@+id/conversation_header" android:layout_width="wrap_content" android:layout_width="wrap_content" Loading Loading @@ -119,6 +119,7 @@ android:layout_width="wrap_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" android:layout_weight="100" android:showRelative="true" android:showRelative="true" android:singleLine="true" android:singleLine="true" android:visibility="gone" android:visibility="gone" Loading Loading @@ -171,4 +172,4 @@ android:src="@drawable/ic_notifications_alerted" android:src="@drawable/ic_notifications_alerted" android:visibility="gone" android:visibility="gone" /> /> </LinearLayout> </com.android.internal.widget.ConversationHeaderLinearLayout> Loading
core/java/com/android/internal/widget/ConversationHeaderLinearLayout.java 0 → 100644 +179 −0 Original line number Original line Diff line number Diff line /* * 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(); } } }
core/res/res/layout/notification_template_conversation_header.xml +3 −2 Original line number Original line Diff line number Diff line Loading @@ -14,7 +14,7 @@ ~ See the License for the specific language governing permissions and ~ See the License for the specific language governing permissions and ~ limitations under the License ~ limitations under the License --> --> <LinearLayout <com.android.internal.widget.ConversationHeaderLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/conversation_header" android:id="@+id/conversation_header" android:layout_width="wrap_content" android:layout_width="wrap_content" Loading Loading @@ -119,6 +119,7 @@ android:layout_width="wrap_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" android:layout_weight="100" android:showRelative="true" android:showRelative="true" android:singleLine="true" android:singleLine="true" android:visibility="gone" android:visibility="gone" Loading Loading @@ -171,4 +172,4 @@ android:src="@drawable/ic_notifications_alerted" android:src="@drawable/ic_notifications_alerted" android:visibility="gone" android:visibility="gone" /> /> </LinearLayout> </com.android.internal.widget.ConversationHeaderLinearLayout>