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