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