blob: 5729b53e98d7a89a37e60beda7964ae8ced9ede6 [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
19import android.content.Context;
Lucas Dupinbd9798f2017-10-24 18:04:51 -070020import android.content.res.TypedArray;
John Reck4aef5262017-12-07 14:55:26 -080021import android.graphics.drawable.RippleDrawable;
Adrian Roos61254352016-04-25 15:45:04 -070022import android.util.AttributeSet;
23import android.util.Pair;
24import android.view.Gravity;
Selim Cinek06e9e1f2016-07-08 17:14:16 -070025import android.view.RemotableViewMethod;
Adrian Roos61254352016-04-25 15:45:04 -070026import android.view.View;
Selim Cinek396caca2018-04-10 17:46:46 -070027import android.view.ViewGroup;
Selim Cinek06e9e1f2016-07-08 17:14:16 -070028import android.widget.LinearLayout;
Adrian Roos61254352016-04-25 15:45:04 -070029import android.widget.RemoteViews;
30import android.widget.TextView;
31
32import java.util.ArrayList;
33import java.util.Comparator;
34
35/**
36 * Layout for notification actions that ensures that no action consumes more than their share of
37 * the remaining available width, and the last action consumes the remaining space.
38 */
39@RemoteViews.RemoteView
Selim Cinek06e9e1f2016-07-08 17:14:16 -070040public class NotificationActionListLayout extends LinearLayout {
Adrian Roos61254352016-04-25 15:45:04 -070041
Lucas Dupinbd9798f2017-10-24 18:04:51 -070042 private final int mGravity;
Adrian Roos61254352016-04-25 15:45:04 -070043 private int mTotalWidth = 0;
44 private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>();
45 private ArrayList<View> mMeasureOrderOther = new ArrayList<>();
Selim Cinek396caca2018-04-10 17:46:46 -070046 private boolean mEmphasizedMode;
47 private int mDefaultPaddingBottom;
48 private int mDefaultPaddingTop;
49 private int mEmphasizedHeight;
50 private int mRegularHeight;
Adrian Roos61254352016-04-25 15:45:04 -070051
52 public NotificationActionListLayout(Context context, AttributeSet attrs) {
Lucas Dupinbd9798f2017-10-24 18:04:51 -070053 this(context, attrs, 0);
54 }
55
56 public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr) {
57 this(context, attrs, defStyleAttr, 0);
58 }
59
60 public NotificationActionListLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
61 super(context, attrs, defStyleAttr, defStyleRes);
62
63 int[] attrIds = { android.R.attr.gravity };
64 TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
65 mGravity = ta.getInt(0, 0);
66 ta.recycle();
Adrian Roos61254352016-04-25 15:45:04 -070067 }
68
69 @Override
70 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Selim Cinek396caca2018-04-10 17:46:46 -070071 if (mEmphasizedMode) {
Selim Cinek06e9e1f2016-07-08 17:14:16 -070072 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
73 return;
74 }
Adrian Roos61254352016-04-25 15:45:04 -070075 final int N = getChildCount();
76 int textViews = 0;
77 int otherViews = 0;
78 int notGoneChildren = 0;
79
80 View lastNotGoneChild = null;
81 for (int i = 0; i < N; i++) {
82 View c = getChildAt(i);
83 if (c instanceof TextView) {
84 textViews++;
85 } else {
86 otherViews++;
87 }
88 if (c.getVisibility() != GONE) {
89 notGoneChildren++;
90 lastNotGoneChild = c;
91 }
92 }
93
94 // Rebuild the measure order if the number of children changed or the text length of
95 // any of the children changed.
96 boolean needRebuild = false;
97 if (textViews != mMeasureOrderTextViews.size()
98 || otherViews != mMeasureOrderOther.size()) {
99 needRebuild = true;
100 }
101 if (!needRebuild) {
102 final int size = mMeasureOrderTextViews.size();
103 for (int i = 0; i < size; i++) {
104 Pair<Integer, TextView> pair = mMeasureOrderTextViews.get(i);
105 if (pair.first != pair.second.getText().length()) {
106 needRebuild = true;
107 }
108 }
109 }
Lucas Dupinfc7036c2018-04-12 17:45:29 -0700110 boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
111 boolean singleChildCentered = notGoneChildren == 1 && centerAligned;
112 boolean needsRegularMeasurement = notGoneChildren > 1 || singleChildCentered;
113
114 if (needsRegularMeasurement && needRebuild) {
Adrian Roos61254352016-04-25 15:45:04 -0700115 rebuildMeasureOrder(textViews, otherViews);
116 }
117
118 final boolean constrained =
119 MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED;
120
121 final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
122 final int otherSize = mMeasureOrderOther.size();
123 int usedWidth = 0;
124
Adrian Roos61254352016-04-25 15:45:04 -0700125 int measuredChildren = 0;
Lucas Dupinfc7036c2018-04-12 17:45:29 -0700126 for (int i = 0; i < N && needsRegularMeasurement; i++) {
Adrian Roos61254352016-04-25 15:45:04 -0700127 // Measure shortest children first. To avoid measuring twice, we approximate by looking
128 // at the text length.
129 View c;
130 if (i < otherSize) {
131 c = mMeasureOrderOther.get(i);
132 } else {
133 c = mMeasureOrderTextViews.get(i - otherSize).second;
134 }
135 if (c.getVisibility() == GONE) {
136 continue;
137 }
138 MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams();
139
140 int usedWidthForChild = usedWidth;
141 if (constrained) {
142 // Make sure that this child doesn't consume more than its share of the remaining
143 // total available space. Not used space will benefit subsequent views. Since we
144 // measure in the order of (approx.) size, a large view can still take more than its
145 // share if the others are small.
146 int availableWidth = innerWidth - usedWidth;
147 int maxWidthForChild = availableWidth / (notGoneChildren - measuredChildren);
148
149 usedWidthForChild = innerWidth - maxWidthForChild;
150 }
151
152 measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild,
153 heightMeasureSpec, 0 /* usedHeight */);
154
155 usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
156 measuredChildren++;
157 }
158
159 // Make sure to measure the last child full-width if we didn't use up the entire width,
160 // or we didn't measure yet because there's just one child.
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700161 if (lastNotGoneChild != null && !centerAligned && (constrained && usedWidth < innerWidth
Adrian Roos61254352016-04-25 15:45:04 -0700162 || notGoneChildren == 1)) {
163 MarginLayoutParams lp = (MarginLayoutParams) lastNotGoneChild.getLayoutParams();
164 if (notGoneChildren > 1) {
165 // Need to make room, since we already measured this once.
166 usedWidth -= lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
167 }
168
169 int originalWidth = lp.width;
170 lp.width = LayoutParams.MATCH_PARENT;
171 measureChildWithMargins(lastNotGoneChild, widthMeasureSpec, usedWidth,
172 heightMeasureSpec, 0 /* usedHeight */);
173 lp.width = originalWidth;
174
175 usedWidth += lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
176 }
177
178 mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft;
179 setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
180 resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
181 }
182
183 private void rebuildMeasureOrder(int capacityText, int capacityOther) {
184 clearMeasureOrder();
185 mMeasureOrderTextViews.ensureCapacity(capacityText);
186 mMeasureOrderOther.ensureCapacity(capacityOther);
187 final int childCount = getChildCount();
188 for (int i = 0; i < childCount; i++) {
189 View c = getChildAt(i);
190 if (c instanceof TextView && ((TextView) c).getText().length() > 0) {
191 mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(),
192 (TextView)c));
193 } else {
194 mMeasureOrderOther.add(c);
195 }
196 }
197 mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR);
198 }
199
200 private void clearMeasureOrder() {
201 mMeasureOrderOther.clear();
202 mMeasureOrderTextViews.clear();
203 }
204
205 @Override
206 public void onViewAdded(View child) {
207 super.onViewAdded(child);
208 clearMeasureOrder();
John Reck4aef5262017-12-07 14:55:26 -0800209 // For some reason ripples + notification actions seem to be an unhappy combination
210 // b/69474443 so just turn them off for now.
211 if (child.getBackground() instanceof RippleDrawable) {
212 ((RippleDrawable)child.getBackground()).setForceSoftware(true);
213 }
Adrian Roos61254352016-04-25 15:45:04 -0700214 }
215
216 @Override
217 public void onViewRemoved(View child) {
218 super.onViewRemoved(child);
219 clearMeasureOrder();
220 }
221
222 @Override
223 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Selim Cinek396caca2018-04-10 17:46:46 -0700224 if (mEmphasizedMode) {
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700225 super.onLayout(changed, left, top, right, bottom);
226 return;
227 }
Adrian Roos61254352016-04-25 15:45:04 -0700228 final boolean isLayoutRtl = isLayoutRtl();
229 final int paddingTop = mPaddingTop;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700230 final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
Adrian Roos61254352016-04-25 15:45:04 -0700231
232 int childTop;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700233 int childLeft = centerAligned ? left + (right - left) / 2 - mTotalWidth / 2 : 0;
Adrian Roos61254352016-04-25 15:45:04 -0700234
235 // Where bottom of child should go
236 final int height = bottom - top;
237
238 // Space available for child
239 int innerHeight = height - paddingTop - mPaddingBottom;
240
241 final int count = getChildCount();
242
243 final int layoutDirection = getLayoutDirection();
244 switch (Gravity.getAbsoluteGravity(Gravity.START, layoutDirection)) {
245 case Gravity.RIGHT:
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700246 childLeft += mPaddingLeft + right - left - mTotalWidth;
Adrian Roos61254352016-04-25 15:45:04 -0700247 break;
248
249 case Gravity.LEFT:
250 default:
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700251 childLeft += mPaddingLeft;
Adrian Roos61254352016-04-25 15:45:04 -0700252 break;
253 }
254
255 int start = 0;
256 int dir = 1;
257 //In case of RTL, start drawing from the last child.
258 if (isLayoutRtl) {
259 start = count - 1;
260 dir = -1;
261 }
262
263 for (int i = 0; i < count; i++) {
264 final int childIndex = start + dir * i;
265 final View child = getChildAt(childIndex);
266 if (child.getVisibility() != GONE) {
267 final int childWidth = child.getMeasuredWidth();
268 final int childHeight = child.getMeasuredHeight();
269
270 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
271
272 childTop = paddingTop + ((innerHeight - childHeight) / 2)
273 + lp.topMargin - lp.bottomMargin;
274
275 childLeft += lp.leftMargin;
276 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
277 childLeft += childWidth + lp.rightMargin;
278 }
279 }
280 }
281
282 @Override
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700283 protected void onFinishInflate() {
284 super.onFinishInflate();
Selim Cinek396caca2018-04-10 17:46:46 -0700285 mDefaultPaddingBottom = getPaddingBottom();
286 mDefaultPaddingTop = getPaddingTop();
287 updateHeights();
288 }
289
290 private void updateHeights() {
291 int paddingTop = getResources().getDimensionPixelSize(
292 com.android.internal.R.dimen.notification_content_margin);
293 // same padding on bottom and at end
294 int paddingBottom = getResources().getDimensionPixelSize(
295 com.android.internal.R.dimen.notification_content_margin_end);
296 mEmphasizedHeight = paddingBottom + paddingTop + getResources().getDimensionPixelSize(
297 com.android.internal.R.dimen.notification_action_emphasized_height);
298 mRegularHeight = getResources().getDimensionPixelSize(
299 com.android.internal.R.dimen.notification_action_list_height);
Adrian Roos61254352016-04-25 15:45:04 -0700300 }
301
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700302 /**
303 * Set whether the list is in a mode where some actions are emphasized. This will trigger an
304 * equal measuring where all actions are full height and change a few parameters like
305 * the padding.
306 */
307 @RemotableViewMethod
308 public void setEmphasizedMode(boolean emphasizedMode) {
Selim Cinek396caca2018-04-10 17:46:46 -0700309 mEmphasizedMode = emphasizedMode;
310 int height;
311 if (emphasizedMode) {
312 int paddingTop = getResources().getDimensionPixelSize(
313 com.android.internal.R.dimen.notification_content_margin);
314 // same padding on bottom and at end
315 int paddingBottom = getResources().getDimensionPixelSize(
316 com.android.internal.R.dimen.notification_content_margin_end);
317 height = mEmphasizedHeight;
318 int buttonPaddingInternal = getResources().getDimensionPixelSize(
319 com.android.internal.R.dimen.button_inset_vertical_material);
320 setPaddingRelative(getPaddingStart(),
321 paddingTop - buttonPaddingInternal,
322 getPaddingEnd(),
323 paddingBottom - buttonPaddingInternal);
324 } else {
325 setPaddingRelative(getPaddingStart(),
326 mDefaultPaddingTop,
327 getPaddingEnd(),
328 mDefaultPaddingBottom);
329 height = mRegularHeight;
330 }
331 ViewGroup.LayoutParams layoutParams = getLayoutParams();
332 layoutParams.height = height;
333 setLayoutParams(layoutParams);
334 }
335
336 public int getExtraMeasureHeight() {
337 if (mEmphasizedMode) {
338 return mEmphasizedHeight - mRegularHeight;
339 }
340 return 0;
Adrian Roos61254352016-04-25 15:45:04 -0700341 }
342
343 public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR
344 = (a, b) -> a.first.compareTo(b.first);
345}