1/* 2 * Copyright (C) 2016 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 17package com.android.internal.widget; 18 19import android.content.Context; 20import android.util.AttributeSet; 21import android.util.Pair; 22import android.view.Gravity; 23import android.view.View; 24import android.view.ViewGroup; 25import android.widget.RemoteViews; 26import android.widget.TextView; 27 28import java.util.ArrayList; 29import java.util.Comparator; 30 31/** 32 * Layout for notification actions that ensures that no action consumes more than their share of 33 * the remaining available width, and the last action consumes the remaining space. 34 */ 35@RemoteViews.RemoteView 36public class NotificationActionListLayout extends ViewGroup { 37 38 private int mTotalWidth = 0; 39 private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>(); 40 private ArrayList<View> mMeasureOrderOther = new ArrayList<>(); 41 42 public NotificationActionListLayout(Context context, AttributeSet attrs) { 43 super(context, attrs); 44 } 45 46 @Override 47 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 48 final int N = getChildCount(); 49 int textViews = 0; 50 int otherViews = 0; 51 int notGoneChildren = 0; 52 53 View lastNotGoneChild = null; 54 for (int i = 0; i < N; i++) { 55 View c = getChildAt(i); 56 if (c instanceof TextView) { 57 textViews++; 58 } else { 59 otherViews++; 60 } 61 if (c.getVisibility() != GONE) { 62 notGoneChildren++; 63 lastNotGoneChild = c; 64 } 65 } 66 67 // Rebuild the measure order if the number of children changed or the text length of 68 // any of the children changed. 69 boolean needRebuild = false; 70 if (textViews != mMeasureOrderTextViews.size() 71 || otherViews != mMeasureOrderOther.size()) { 72 needRebuild = true; 73 } 74 if (!needRebuild) { 75 final int size = mMeasureOrderTextViews.size(); 76 for (int i = 0; i < size; i++) { 77 Pair<Integer, TextView> pair = mMeasureOrderTextViews.get(i); 78 if (pair.first != pair.second.getText().length()) { 79 needRebuild = true; 80 } 81 } 82 } 83 if (notGoneChildren > 1 && needRebuild) { 84 rebuildMeasureOrder(textViews, otherViews); 85 } 86 87 final boolean constrained = 88 MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED; 89 90 final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight; 91 final int otherSize = mMeasureOrderOther.size(); 92 int usedWidth = 0; 93 94 // Optimization: Don't do this if there's only one child. 95 int measuredChildren = 0; 96 for (int i = 0; i < N && notGoneChildren > 1; i++) { 97 // Measure shortest children first. To avoid measuring twice, we approximate by looking 98 // at the text length. 99 View c; 100 if (i < otherSize) { 101 c = mMeasureOrderOther.get(i); 102 } else { 103 c = mMeasureOrderTextViews.get(i - otherSize).second; 104 } 105 if (c.getVisibility() == GONE) { 106 continue; 107 } 108 MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams(); 109 110 int usedWidthForChild = usedWidth; 111 if (constrained) { 112 // Make sure that this child doesn't consume more than its share of the remaining 113 // total available space. Not used space will benefit subsequent views. Since we 114 // measure in the order of (approx.) size, a large view can still take more than its 115 // share if the others are small. 116 int availableWidth = innerWidth - usedWidth; 117 int maxWidthForChild = availableWidth / (notGoneChildren - measuredChildren); 118 119 usedWidthForChild = innerWidth - maxWidthForChild; 120 } 121 122 measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild, 123 heightMeasureSpec, 0 /* usedHeight */); 124 125 usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin; 126 measuredChildren++; 127 } 128 129 // Make sure to measure the last child full-width if we didn't use up the entire width, 130 // or we didn't measure yet because there's just one child. 131 if (lastNotGoneChild != null && (constrained && usedWidth < innerWidth 132 || notGoneChildren == 1)) { 133 MarginLayoutParams lp = (MarginLayoutParams) lastNotGoneChild.getLayoutParams(); 134 if (notGoneChildren > 1) { 135 // Need to make room, since we already measured this once. 136 usedWidth -= lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin; 137 } 138 139 int originalWidth = lp.width; 140 lp.width = LayoutParams.MATCH_PARENT; 141 measureChildWithMargins(lastNotGoneChild, widthMeasureSpec, usedWidth, 142 heightMeasureSpec, 0 /* usedHeight */); 143 lp.width = originalWidth; 144 145 usedWidth += lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin; 146 } 147 148 mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft; 149 setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec), 150 resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 151 } 152 153 private void rebuildMeasureOrder(int capacityText, int capacityOther) { 154 clearMeasureOrder(); 155 mMeasureOrderTextViews.ensureCapacity(capacityText); 156 mMeasureOrderOther.ensureCapacity(capacityOther); 157 final int childCount = getChildCount(); 158 for (int i = 0; i < childCount; i++) { 159 View c = getChildAt(i); 160 if (c instanceof TextView && ((TextView) c).getText().length() > 0) { 161 mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(), 162 (TextView)c)); 163 } else { 164 mMeasureOrderOther.add(c); 165 } 166 } 167 mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR); 168 } 169 170 private void clearMeasureOrder() { 171 mMeasureOrderOther.clear(); 172 mMeasureOrderTextViews.clear(); 173 } 174 175 @Override 176 public void onViewAdded(View child) { 177 super.onViewAdded(child); 178 clearMeasureOrder(); 179 } 180 181 @Override 182 public void onViewRemoved(View child) { 183 super.onViewRemoved(child); 184 clearMeasureOrder(); 185 } 186 187 @Override 188 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 189 final boolean isLayoutRtl = isLayoutRtl(); 190 final int paddingTop = mPaddingTop; 191 192 int childTop; 193 int childLeft; 194 195 // Where bottom of child should go 196 final int height = bottom - top; 197 198 // Space available for child 199 int innerHeight = height - paddingTop - mPaddingBottom; 200 201 final int count = getChildCount(); 202 203 final int layoutDirection = getLayoutDirection(); 204 switch (Gravity.getAbsoluteGravity(Gravity.START, layoutDirection)) { 205 case Gravity.RIGHT: 206 // mTotalWidth contains the padding already 207 childLeft = mPaddingLeft + right - left - mTotalWidth; 208 break; 209 210 case Gravity.LEFT: 211 default: 212 childLeft = mPaddingLeft; 213 break; 214 } 215 216 int start = 0; 217 int dir = 1; 218 //In case of RTL, start drawing from the last child. 219 if (isLayoutRtl) { 220 start = count - 1; 221 dir = -1; 222 } 223 224 for (int i = 0; i < count; i++) { 225 final int childIndex = start + dir * i; 226 final View child = getChildAt(childIndex); 227 if (child.getVisibility() != GONE) { 228 final int childWidth = child.getMeasuredWidth(); 229 final int childHeight = child.getMeasuredHeight(); 230 231 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 232 233 childTop = paddingTop + ((innerHeight - childHeight) / 2) 234 + lp.topMargin - lp.bottomMargin; 235 236 childLeft += lp.leftMargin; 237 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); 238 childLeft += childWidth + lp.rightMargin; 239 } 240 } 241 } 242 243 @Override 244 public LayoutParams generateLayoutParams(AttributeSet attrs) { 245 return new MarginLayoutParams(getContext(), attrs); 246 } 247 248 @Override 249 protected LayoutParams generateDefaultLayoutParams() { 250 return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); 251 } 252 253 @Override 254 protected LayoutParams generateLayoutParams(LayoutParams p) { 255 if (p instanceof MarginLayoutParams) { 256 return new MarginLayoutParams((MarginLayoutParams)p); 257 } 258 return new MarginLayoutParams(p); 259 } 260 261 @Override 262 protected boolean checkLayoutParams(LayoutParams p) { 263 return p instanceof MarginLayoutParams; 264 } 265 266 public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR 267 = (a, b) -> a.first.compareTo(b.first); 268} 269