blob: 0482f6cf596ac318d4ae14d6d5b941201cad9233 [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 }
110 if (notGoneChildren > 1 && needRebuild) {
111 rebuildMeasureOrder(textViews, otherViews);
112 }
113
114 final boolean constrained =
115 MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700116 final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
Adrian Roos61254352016-04-25 15:45:04 -0700117
118 final int innerWidth = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
119 final int otherSize = mMeasureOrderOther.size();
120 int usedWidth = 0;
121
122 // Optimization: Don't do this if there's only one child.
123 int measuredChildren = 0;
124 for (int i = 0; i < N && notGoneChildren > 1; i++) {
125 // Measure shortest children first. To avoid measuring twice, we approximate by looking
126 // at the text length.
127 View c;
128 if (i < otherSize) {
129 c = mMeasureOrderOther.get(i);
130 } else {
131 c = mMeasureOrderTextViews.get(i - otherSize).second;
132 }
133 if (c.getVisibility() == GONE) {
134 continue;
135 }
136 MarginLayoutParams lp = (MarginLayoutParams) c.getLayoutParams();
137
138 int usedWidthForChild = usedWidth;
139 if (constrained) {
140 // Make sure that this child doesn't consume more than its share of the remaining
141 // total available space. Not used space will benefit subsequent views. Since we
142 // measure in the order of (approx.) size, a large view can still take more than its
143 // share if the others are small.
144 int availableWidth = innerWidth - usedWidth;
145 int maxWidthForChild = availableWidth / (notGoneChildren - measuredChildren);
146
147 usedWidthForChild = innerWidth - maxWidthForChild;
148 }
149
150 measureChildWithMargins(c, widthMeasureSpec, usedWidthForChild,
151 heightMeasureSpec, 0 /* usedHeight */);
152
153 usedWidth += c.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
154 measuredChildren++;
155 }
156
157 // Make sure to measure the last child full-width if we didn't use up the entire width,
158 // or we didn't measure yet because there's just one child.
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700159 if (lastNotGoneChild != null && !centerAligned && (constrained && usedWidth < innerWidth
Adrian Roos61254352016-04-25 15:45:04 -0700160 || notGoneChildren == 1)) {
161 MarginLayoutParams lp = (MarginLayoutParams) lastNotGoneChild.getLayoutParams();
162 if (notGoneChildren > 1) {
163 // Need to make room, since we already measured this once.
164 usedWidth -= lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
165 }
166
167 int originalWidth = lp.width;
168 lp.width = LayoutParams.MATCH_PARENT;
169 measureChildWithMargins(lastNotGoneChild, widthMeasureSpec, usedWidth,
170 heightMeasureSpec, 0 /* usedHeight */);
171 lp.width = originalWidth;
172
173 usedWidth += lastNotGoneChild.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
174 }
175
176 mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft;
177 setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec),
178 resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec));
179 }
180
181 private void rebuildMeasureOrder(int capacityText, int capacityOther) {
182 clearMeasureOrder();
183 mMeasureOrderTextViews.ensureCapacity(capacityText);
184 mMeasureOrderOther.ensureCapacity(capacityOther);
185 final int childCount = getChildCount();
186 for (int i = 0; i < childCount; i++) {
187 View c = getChildAt(i);
188 if (c instanceof TextView && ((TextView) c).getText().length() > 0) {
189 mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(),
190 (TextView)c));
191 } else {
192 mMeasureOrderOther.add(c);
193 }
194 }
195 mMeasureOrderTextViews.sort(MEASURE_ORDER_COMPARATOR);
196 }
197
198 private void clearMeasureOrder() {
199 mMeasureOrderOther.clear();
200 mMeasureOrderTextViews.clear();
201 }
202
203 @Override
204 public void onViewAdded(View child) {
205 super.onViewAdded(child);
206 clearMeasureOrder();
John Reck4aef5262017-12-07 14:55:26 -0800207 // For some reason ripples + notification actions seem to be an unhappy combination
208 // b/69474443 so just turn them off for now.
209 if (child.getBackground() instanceof RippleDrawable) {
210 ((RippleDrawable)child.getBackground()).setForceSoftware(true);
211 }
Adrian Roos61254352016-04-25 15:45:04 -0700212 }
213
214 @Override
215 public void onViewRemoved(View child) {
216 super.onViewRemoved(child);
217 clearMeasureOrder();
218 }
219
220 @Override
221 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Selim Cinek396caca2018-04-10 17:46:46 -0700222 if (mEmphasizedMode) {
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700223 super.onLayout(changed, left, top, right, bottom);
224 return;
225 }
Adrian Roos61254352016-04-25 15:45:04 -0700226 final boolean isLayoutRtl = isLayoutRtl();
227 final int paddingTop = mPaddingTop;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700228 final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
Adrian Roos61254352016-04-25 15:45:04 -0700229
230 int childTop;
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700231 int childLeft = centerAligned ? left + (right - left) / 2 - mTotalWidth / 2 : 0;
Adrian Roos61254352016-04-25 15:45:04 -0700232
233 // Where bottom of child should go
234 final int height = bottom - top;
235
236 // Space available for child
237 int innerHeight = height - paddingTop - mPaddingBottom;
238
239 final int count = getChildCount();
240
241 final int layoutDirection = getLayoutDirection();
242 switch (Gravity.getAbsoluteGravity(Gravity.START, layoutDirection)) {
243 case Gravity.RIGHT:
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700244 childLeft += mPaddingLeft + right - left - mTotalWidth;
Adrian Roos61254352016-04-25 15:45:04 -0700245 break;
246
247 case Gravity.LEFT:
248 default:
Lucas Dupinbd9798f2017-10-24 18:04:51 -0700249 childLeft += mPaddingLeft;
Adrian Roos61254352016-04-25 15:45:04 -0700250 break;
251 }
252
253 int start = 0;
254 int dir = 1;
255 //In case of RTL, start drawing from the last child.
256 if (isLayoutRtl) {
257 start = count - 1;
258 dir = -1;
259 }
260
261 for (int i = 0; i < count; i++) {
262 final int childIndex = start + dir * i;
263 final View child = getChildAt(childIndex);
264 if (child.getVisibility() != GONE) {
265 final int childWidth = child.getMeasuredWidth();
266 final int childHeight = child.getMeasuredHeight();
267
268 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
269
270 childTop = paddingTop + ((innerHeight - childHeight) / 2)
271 + lp.topMargin - lp.bottomMargin;
272
273 childLeft += lp.leftMargin;
274 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
275 childLeft += childWidth + lp.rightMargin;
276 }
277 }
278 }
279
280 @Override
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700281 protected void onFinishInflate() {
282 super.onFinishInflate();
Selim Cinek396caca2018-04-10 17:46:46 -0700283 mDefaultPaddingBottom = getPaddingBottom();
284 mDefaultPaddingTop = getPaddingTop();
285 updateHeights();
286 }
287
288 private void updateHeights() {
289 int paddingTop = getResources().getDimensionPixelSize(
290 com.android.internal.R.dimen.notification_content_margin);
291 // same padding on bottom and at end
292 int paddingBottom = getResources().getDimensionPixelSize(
293 com.android.internal.R.dimen.notification_content_margin_end);
294 mEmphasizedHeight = paddingBottom + paddingTop + getResources().getDimensionPixelSize(
295 com.android.internal.R.dimen.notification_action_emphasized_height);
296 mRegularHeight = getResources().getDimensionPixelSize(
297 com.android.internal.R.dimen.notification_action_list_height);
Adrian Roos61254352016-04-25 15:45:04 -0700298 }
299
Selim Cinek06e9e1f2016-07-08 17:14:16 -0700300 /**
301 * Set whether the list is in a mode where some actions are emphasized. This will trigger an
302 * equal measuring where all actions are full height and change a few parameters like
303 * the padding.
304 */
305 @RemotableViewMethod
306 public void setEmphasizedMode(boolean emphasizedMode) {
Selim Cinek396caca2018-04-10 17:46:46 -0700307 mEmphasizedMode = emphasizedMode;
308 int height;
309 if (emphasizedMode) {
310 int paddingTop = getResources().getDimensionPixelSize(
311 com.android.internal.R.dimen.notification_content_margin);
312 // same padding on bottom and at end
313 int paddingBottom = getResources().getDimensionPixelSize(
314 com.android.internal.R.dimen.notification_content_margin_end);
315 height = mEmphasizedHeight;
316 int buttonPaddingInternal = getResources().getDimensionPixelSize(
317 com.android.internal.R.dimen.button_inset_vertical_material);
318 setPaddingRelative(getPaddingStart(),
319 paddingTop - buttonPaddingInternal,
320 getPaddingEnd(),
321 paddingBottom - buttonPaddingInternal);
322 } else {
323 setPaddingRelative(getPaddingStart(),
324 mDefaultPaddingTop,
325 getPaddingEnd(),
326 mDefaultPaddingBottom);
327 height = mRegularHeight;
328 }
329 ViewGroup.LayoutParams layoutParams = getLayoutParams();
330 layoutParams.height = height;
331 setLayoutParams(layoutParams);
332 }
333
334 public int getExtraMeasureHeight() {
335 if (mEmphasizedMode) {
336 return mEmphasizedHeight - mRegularHeight;
337 }
338 return 0;
Adrian Roos61254352016-04-25 15:45:04 -0700339 }
340
341 public static final Comparator<Pair<Integer, TextView>> MEASURE_ORDER_COMPARATOR
342 = (a, b) -> a.first.compareTo(b.first);
343}