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