1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.widget;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.util.AttributeSet;
22 import android.view.View;
23 import android.widget.LinearLayout;
24 import android.widget.RemoteViews;
25 
26 import java.util.ArrayList;
27 import java.util.LinkedList;
28 import java.util.List;
29 
30 /**
31  * This is a subclass of LinearLayout meant to be used in the Conversation header, to fix a bug
32  * when multiple user-provided strings are shown in the same conversation header.  b/189723284
33  *
34  * This works around a deficiency in LinearLayout when shrinking views that it can't fully reduce
35  * all contents if any of the oversized views reaches zero.
36  */
37 @RemoteViews.RemoteView
38 public class ConversationHeaderLinearLayout extends LinearLayout {
39 
ConversationHeaderLinearLayout(Context context)40     public ConversationHeaderLinearLayout(Context context) {
41         super(context);
42     }
43 
ConversationHeaderLinearLayout(Context context, @Nullable AttributeSet attrs)44     public ConversationHeaderLinearLayout(Context context,
45             @Nullable AttributeSet attrs) {
46         super(context, attrs);
47     }
48 
ConversationHeaderLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr)49     public ConversationHeaderLinearLayout(Context context, @Nullable AttributeSet attrs,
50             int defStyleAttr) {
51         super(context, attrs, defStyleAttr);
52     }
53 
calculateTotalChildLength()54     private int calculateTotalChildLength() {
55         final int count = getChildCount();
56         int totalLength = 0;
57 
58         for (int i = 0; i < count; ++i) {
59             final View child = getChildAt(i);
60             if (child == null || child.getVisibility() == GONE) {
61                 continue;
62             }
63             final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
64                     child.getLayoutParams();
65             totalLength += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
66         }
67         return totalLength + getPaddingLeft() + getPaddingRight();
68     }
69 
70     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)71     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
72         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
73 
74         final int containerWidth = getMeasuredWidth();
75         final int contentsWidth = calculateTotalChildLength();
76 
77         int excessContents = contentsWidth - containerWidth;
78         if (excessContents <= 0) {
79             return;
80         }
81         final int count = getChildCount();
82 
83         float remainingWeight = 0;
84         List<ViewInfo> visibleChildrenToShorten = null;
85 
86         // Find children which need to be shortened in order to ensure the contents fit.
87         for (int i = 0; i < count; ++i) {
88             final View child = getChildAt(i);
89             if (child == null || child.getVisibility() == View.GONE) {
90                 continue;
91             }
92             final float weight = ((LayoutParams) child.getLayoutParams()).weight;
93             if (weight == 0) {
94                 continue;
95             }
96             if (child.getMeasuredWidth() == 0) {
97                 continue;
98             }
99             if (visibleChildrenToShorten == null) {
100                 visibleChildrenToShorten = new ArrayList<>(count);
101             }
102             visibleChildrenToShorten.add(new ViewInfo(child));
103             remainingWeight += Math.max(0, weight);
104         }
105         if (visibleChildrenToShorten == null || visibleChildrenToShorten.isEmpty()) {
106             return;
107         }
108         balanceViewWidths(visibleChildrenToShorten, remainingWeight, excessContents);
109         remeasureChangedChildren(visibleChildrenToShorten);
110     }
111 
112     /**
113      * Measure any child with a width that has changed.
114      */
remeasureChangedChildren(List<ViewInfo> childrenInfo)115     private void remeasureChangedChildren(List<ViewInfo> childrenInfo) {
116         for (ViewInfo info : childrenInfo) {
117             if (info.mWidth != info.mStartWidth) {
118                 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
119                         Math.max(0, info.mWidth), MeasureSpec.EXACTLY);
120                 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
121                         info.mView.getMeasuredHeight(), MeasureSpec.EXACTLY);
122                 info.mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
123             }
124         }
125     }
126 
127     /**
128      * Given a list of view, use the weights to remove width from each view proportionally to the
129      * weight (and ignoring the view's actual width), but do this iteratively whenever a view is
130      * reduced to zero width, because in that case other views need reduction.
131      */
balanceViewWidths(List<ViewInfo> viewInfos, float weightSum, int excessContents)132     void balanceViewWidths(List<ViewInfo> viewInfos, float weightSum, int excessContents) {
133         boolean performAnotherPass = true;
134         // Loops only when all of the following are true:
135         // * `performAnotherPass` -- a view clamped to 0 width (or the first iteration)
136         // * `excessContents > 0` -- there is still horizontal space to allocate
137         // * `weightSum > 0` -- at least 1 view with nonzero width AND nonzero weight left
138         while (performAnotherPass && excessContents > 0 && weightSum > 0) {
139             int excessRemovedDuringThisPass = 0;
140             float weightSumForNextPass = 0;
141             performAnotherPass = false;
142             for (ViewInfo info : viewInfos) {
143                 if (info.mWeight <= 0) {
144                     continue;
145                 }
146                 if (info.mWidth <= 0) {
147                     continue;
148                 }
149                 int newWidth = (int) (info.mWidth - (excessContents * (info.mWeight / weightSum)));
150                 if (newWidth < 0) {
151                     newWidth = 0;
152                     performAnotherPass = true;
153                 }
154                 excessRemovedDuringThisPass += info.mWidth - newWidth;
155                 info.mWidth = newWidth;
156                 if (info.mWidth > 0) {
157                     weightSumForNextPass += info.mWeight;
158                 }
159             }
160             excessContents -= excessRemovedDuringThisPass;
161             weightSum = weightSumForNextPass;
162         }
163     }
164 
165     /**
166      * A helper class for measuring children.
167      */
168     static class ViewInfo {
169         final View mView;
170         final float mWeight;
171         final int mStartWidth;
172         int mWidth;
173 
ViewInfo(View view)174         ViewInfo(View view) {
175             this.mView = view;
176             this.mWeight = ((LayoutParams) view.getLayoutParams()).weight;
177             this.mStartWidth = this.mWidth = view.getMeasuredWidth();
178         }
179     }
180 }
181