blob: 143168210a5177d0050c820948fc6e7458480425 [file] [log] [blame]
Petr Cermaked7429c2017-12-18 19:38:04 +00001package com.android.systemui.statusbar.policy;
2
3import android.app.PendingIntent;
4import android.app.RemoteInput;
5import android.content.Context;
6import android.content.Intent;
Petr Cermak102431d2018-01-29 10:36:07 +00007import android.content.res.TypedArray;
8import android.graphics.Canvas;
9import android.graphics.drawable.GradientDrawable;
10import android.graphics.drawable.RippleDrawable;
Petr Cermaked7429c2017-12-18 19:38:04 +000011import android.os.Bundle;
Petr Cermak102431d2018-01-29 10:36:07 +000012import android.text.Layout;
13import android.text.TextPaint;
14import android.text.method.TransformationMethod;
Petr Cermaked7429c2017-12-18 19:38:04 +000015import android.util.AttributeSet;
16import android.util.Log;
17import android.view.LayoutInflater;
Petr Cermak102431d2018-01-29 10:36:07 +000018import android.view.View;
Petr Cermaked7429c2017-12-18 19:38:04 +000019import android.view.ViewGroup;
Milo Sredkov66da07b2018-04-17 14:04:54 +010020import android.view.accessibility.AccessibilityNodeInfo;
21import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Petr Cermaked7429c2017-12-18 19:38:04 +000022import android.widget.Button;
Petr Cermaked7429c2017-12-18 19:38:04 +000023
Petr Cermak102431d2018-01-29 10:36:07 +000024import com.android.internal.annotations.VisibleForTesting;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010025import com.android.keyguard.KeyguardHostView.OnDismissAction;
Petr Cermak10011fa2018-02-05 19:00:54 +000026import com.android.systemui.Dependency;
Petr Cermaked7429c2017-12-18 19:38:04 +000027import com.android.systemui.R;
Kenny Guy23991102018-04-05 21:18:38 +010028import com.android.systemui.statusbar.NotificationData;
29import com.android.systemui.statusbar.SmartReplyLogger;
Milo Sredkove7cf4982018-04-09 15:08:26 +010030import com.android.systemui.statusbar.notification.NotificationUtils;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010031import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
Petr Cermaked7429c2017-12-18 19:38:04 +000032
Petr Cermak102431d2018-01-29 10:36:07 +000033import java.text.BreakIterator;
34import java.util.Comparator;
35import java.util.PriorityQueue;
36
Petr Cermaked7429c2017-12-18 19:38:04 +000037/** View which displays smart reply buttons in notifications. */
Petr Cermak102431d2018-01-29 10:36:07 +000038public class SmartReplyView extends ViewGroup {
Petr Cermaked7429c2017-12-18 19:38:04 +000039
40 private static final String TAG = "SmartReplyView";
41
Petr Cermak102431d2018-01-29 10:36:07 +000042 private static final int MEASURE_SPEC_ANY_WIDTH =
43 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
44
45 private static final Comparator<View> DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR =
46 (v1, v2) -> ((v2.getMeasuredWidth() - v2.getPaddingLeft() - v2.getPaddingRight())
47 - (v1.getMeasuredWidth() - v1.getPaddingLeft() - v1.getPaddingRight()));
48
49 private static final int SQUEEZE_FAILED = -1;
50
Petr Cermak10011fa2018-02-05 19:00:54 +000051 private final SmartReplyConstants mConstants;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010052 private final KeyguardDismissUtil mKeyguardDismissUtil;
Petr Cermak10011fa2018-02-05 19:00:54 +000053
Milo Sredkove7cf4982018-04-09 15:08:26 +010054 /**
55 * The upper bound for the height of this view in pixels. Notifications are automatically
56 * recreated on density or font size changes so caching this should be fine.
57 */
58 private final int mHeightUpperLimit;
59
Petr Cermak102431d2018-01-29 10:36:07 +000060 /** Spacing to be applied between views. */
61 private final int mSpacing;
62
63 /** Horizontal padding of smart reply buttons if all of them use only one line of text. */
64 private final int mSingleLineButtonPaddingHorizontal;
65
66 /** Horizontal padding of smart reply buttons if at least one of them uses two lines of text. */
67 private final int mDoubleLineButtonPaddingHorizontal;
68
69 /** Increase in width of a smart reply button as a result of using two lines instead of one. */
70 private final int mSingleToDoubleLineButtonWidthIncrease;
71
72 private final BreakIterator mBreakIterator;
73
74 private PriorityQueue<Button> mCandidateButtonQueueForSqueezing;
75
Petr Cermaked7429c2017-12-18 19:38:04 +000076 public SmartReplyView(Context context, AttributeSet attrs) {
77 super(context, attrs);
Petr Cermak10011fa2018-02-05 19:00:54 +000078 mConstants = Dependency.get(SmartReplyConstants.class);
Milo Sredkovb0f55e92018-04-04 16:13:28 +010079 mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class);
Petr Cermak102431d2018-01-29 10:36:07 +000080
Milo Sredkove7cf4982018-04-09 15:08:26 +010081 mHeightUpperLimit = NotificationUtils.getFontScaledHeight(mContext,
82 R.dimen.smart_reply_button_max_height);
83
Petr Cermak102431d2018-01-29 10:36:07 +000084 int spacing = 0;
85 int singleLineButtonPaddingHorizontal = 0;
86 int doubleLineButtonPaddingHorizontal = 0;
87
88 final TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.SmartReplyView,
89 0, 0);
90 final int length = arr.getIndexCount();
91 for (int i = 0; i < length; i++) {
92 int attr = arr.getIndex(i);
93 switch (attr) {
94 case R.styleable.SmartReplyView_spacing:
95 spacing = arr.getDimensionPixelSize(i, 0);
96 break;
97 case R.styleable.SmartReplyView_singleLineButtonPaddingHorizontal:
98 singleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
99 break;
100 case R.styleable.SmartReplyView_doubleLineButtonPaddingHorizontal:
101 doubleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
102 break;
103 }
104 }
105 arr.recycle();
106
107 mSpacing = spacing;
108 mSingleLineButtonPaddingHorizontal = singleLineButtonPaddingHorizontal;
109 mDoubleLineButtonPaddingHorizontal = doubleLineButtonPaddingHorizontal;
110 mSingleToDoubleLineButtonWidthIncrease =
111 2 * (doubleLineButtonPaddingHorizontal - singleLineButtonPaddingHorizontal);
112
Milo Sredkove7cf4982018-04-09 15:08:26 +0100113
Petr Cermak102431d2018-01-29 10:36:07 +0000114 mBreakIterator = BreakIterator.getLineInstance();
115 reallocateCandidateButtonQueueForSqueezing();
116 }
117
Milo Sredkove7cf4982018-04-09 15:08:26 +0100118 /**
119 * Returns an upper bound for the height of this view in pixels. This method is intended to be
120 * invoked before onMeasure, so it doesn't do any analysis on the contents of the buttons.
121 */
122 public int getHeightUpperLimit() {
123 return mHeightUpperLimit;
124 }
125
Petr Cermak102431d2018-01-29 10:36:07 +0000126 private void reallocateCandidateButtonQueueForSqueezing() {
127 // Instead of clearing the priority queue, we re-allocate so that it would fit all buttons
128 // exactly. This avoids (1) wasting memory because PriorityQueue never shrinks and
129 // (2) growing in onMeasure.
130 // The constructor throws an IllegalArgument exception if initial capacity is less than 1.
131 mCandidateButtonQueueForSqueezing = new PriorityQueue<>(
132 Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
Petr Cermaked7429c2017-12-18 19:38:04 +0000133 }
134
Kenny Guy23991102018-04-05 21:18:38 +0100135 public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent,
136 SmartReplyLogger smartReplyLogger, NotificationData.Entry entry) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000137 removeAllViews();
138 if (remoteInput != null && pendingIntent != null) {
139 CharSequence[] choices = remoteInput.getChoices();
140 if (choices != null) {
Kenny Guy23991102018-04-05 21:18:38 +0100141 for (int i = 0; i < choices.length; ++i) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000142 Button replyButton = inflateReplyButton(
Kenny Guy23991102018-04-05 21:18:38 +0100143 getContext(), this, i, choices[i], remoteInput, pendingIntent,
144 smartReplyLogger, entry);
Petr Cermaked7429c2017-12-18 19:38:04 +0000145 addView(replyButton);
146 }
147 }
148 }
Petr Cermak102431d2018-01-29 10:36:07 +0000149 reallocateCandidateButtonQueueForSqueezing();
Petr Cermaked7429c2017-12-18 19:38:04 +0000150 }
151
152 public static SmartReplyView inflate(Context context, ViewGroup root) {
153 return (SmartReplyView)
154 LayoutInflater.from(context).inflate(R.layout.smart_reply_view, root, false);
155 }
156
Petr Cermak102431d2018-01-29 10:36:07 +0000157 @VisibleForTesting
Kenny Guy23991102018-04-05 21:18:38 +0100158 Button inflateReplyButton(Context context, ViewGroup root, int replyIndex,
159 CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent,
160 SmartReplyLogger smartReplyLogger, NotificationData.Entry entry) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000161 Button b = (Button) LayoutInflater.from(context).inflate(
162 R.layout.smart_reply_button, root, false);
163 b.setText(choice);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100164
165 OnDismissAction action = () -> {
Petr Cermaked7429c2017-12-18 19:38:04 +0000166 Bundle results = new Bundle();
167 results.putString(remoteInput.getResultKey(), choice.toString());
168 Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
169 RemoteInput.addResultsToIntent(new RemoteInput[]{remoteInput}, intent, results);
Petr Cermak9a3380c2018-01-19 15:00:24 +0000170 RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
Petr Cermaked7429c2017-12-18 19:38:04 +0000171 try {
172 pendingIntent.send(context, 0, intent);
173 } catch (PendingIntent.CanceledException e) {
174 Log.w(TAG, "Unable to send smart reply", e);
175 }
Kenny Guy23991102018-04-05 21:18:38 +0100176 smartReplyLogger.smartReplySent(entry, replyIndex);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100177 return false; // do not defer
178 };
179
180 b.setOnClickListener(view -> {
181 mKeyguardDismissUtil.dismissKeyguardThenExecute(
182 action, null /* cancelAction */, false /* afterKeyguardGone */);
Petr Cermaked7429c2017-12-18 19:38:04 +0000183 });
Milo Sredkov66da07b2018-04-17 14:04:54 +0100184
185 b.setAccessibilityDelegate(new AccessibilityDelegate() {
186 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
187 super.onInitializeAccessibilityNodeInfo(host, info);
188 String label = getResources().getString(R.string.accessibility_send_smart_reply);
189 info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
190 }
191 });
192
Petr Cermaked7429c2017-12-18 19:38:04 +0000193 return b;
194 }
Petr Cermak102431d2018-01-29 10:36:07 +0000195
196 @Override
197 public LayoutParams generateLayoutParams(AttributeSet attrs) {
198 return new LayoutParams(mContext, attrs);
199 }
200
201 @Override
202 protected LayoutParams generateDefaultLayoutParams() {
203 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
204 }
205
206 @Override
207 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
208 return new LayoutParams(params.width, params.height);
209 }
210
211 @Override
212 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
213 final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
214 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
215
216 // Mark all buttons as hidden and un-squeezed.
217 resetButtonsLayoutParams();
218
219 if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
220 Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
221 mCandidateButtonQueueForSqueezing.clear();
222 }
223
224 int measuredWidth = mPaddingLeft + mPaddingRight;
225 int maxChildHeight = 0;
226 int displayedChildCount = 0;
227 int buttonPaddingHorizontal = mSingleLineButtonPaddingHorizontal;
228
229 final int childCount = getChildCount();
230 for (int i = 0; i < childCount; i++) {
231 final View child = getChildAt(i);
232 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
233 if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
234 continue;
235 }
236
237 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
238 buttonPaddingHorizontal, child.getPaddingBottom());
239 child.measure(MEASURE_SPEC_ANY_WIDTH, heightMeasureSpec);
240
241 final int lineCount = ((Button) child).getLineCount();
242 if (lineCount < 1 || lineCount > 2) {
243 // If smart reply has no text, or more than two lines, then don't show it.
244 continue;
245 }
246
247 if (lineCount == 1) {
248 mCandidateButtonQueueForSqueezing.add((Button) child);
249 }
250
251 // Remember the current measurements in case the current button doesn't fit in.
252 final int originalMaxChildHeight = maxChildHeight;
253 final int originalMeasuredWidth = measuredWidth;
254 final int originalButtonPaddingHorizontal = buttonPaddingHorizontal;
255
256 final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
257 final int childWidth = child.getMeasuredWidth();
258 final int childHeight = child.getMeasuredHeight();
259 measuredWidth += spacing + childWidth;
260 maxChildHeight = Math.max(maxChildHeight, childHeight);
261
262 // Do we need to increase the number of lines in smart reply buttons to two?
263 final boolean increaseToTwoLines =
264 buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal
265 && (lineCount == 2 || measuredWidth > targetWidth);
266 if (increaseToTwoLines) {
267 measuredWidth += (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease;
268 buttonPaddingHorizontal = mDoubleLineButtonPaddingHorizontal;
269 }
270
271 // If the last button doesn't fit into the remaining width, try squeezing preceding
272 // smart reply buttons.
273 if (measuredWidth > targetWidth) {
274 // Keep squeezing preceding and current smart reply buttons until they all fit.
275 while (measuredWidth > targetWidth
276 && !mCandidateButtonQueueForSqueezing.isEmpty()) {
277 final Button candidate = mCandidateButtonQueueForSqueezing.poll();
278 final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
279 if (squeezeReduction != SQUEEZE_FAILED) {
280 maxChildHeight = Math.max(maxChildHeight, candidate.getMeasuredHeight());
281 measuredWidth -= squeezeReduction;
282 }
283 }
284
285 // If the current button still doesn't fit after squeezing all buttons, undo the
286 // last squeezing round.
287 if (measuredWidth > targetWidth) {
288 measuredWidth = originalMeasuredWidth;
289 maxChildHeight = originalMaxChildHeight;
290 buttonPaddingHorizontal = originalButtonPaddingHorizontal;
291
292 // Mark all buttons from the last squeezing round as "failed to squeeze", so
293 // that they're re-measured without squeezing later.
294 markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_FAILED, i);
295
296 // The current button doesn't fit, so there's no point in measuring further
297 // buttons.
298 break;
299 }
300
301 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
302 // to prevent them from being un-squeezed in a subsequent squeezing round.
303 markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, i);
304 }
305
306 lp.show = true;
307 displayedChildCount++;
308 }
309
310 // We're done squeezing buttons, so we can clear the priority queue.
311 mCandidateButtonQueueForSqueezing.clear();
312
Milo Sredkova5bacea2018-04-12 12:52:43 +0100313 // Finally, we need to re-measure some buttons.
314 remeasureButtonsIfNecessary(buttonPaddingHorizontal, maxChildHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000315
316 setMeasuredDimension(
317 resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec),
318 resolveSize(Math.max(getSuggestedMinimumHeight(),
319 mPaddingTop + maxChildHeight + mPaddingBottom), heightMeasureSpec));
320 }
321
322 private void resetButtonsLayoutParams() {
323 final int childCount = getChildCount();
324 for (int i = 0; i < childCount; i++) {
325 final View child = getChildAt(i);
326 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
327 lp.show = false;
328 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
329 }
330 }
331
332 private int squeezeButton(Button button, int heightMeasureSpec) {
333 final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
334 if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
335 return SQUEEZE_FAILED;
336 }
337 return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
338 }
339
340 private int estimateOptimalSqueezedButtonTextWidth(Button button) {
341 // Find a line-break point in the middle of the smart reply button text.
342 final String rawText = button.getText().toString();
343
344 // The button sometimes has a transformation affecting text layout (e.g. all caps).
345 final TransformationMethod transformation = button.getTransformationMethod();
346 final String text = transformation == null ?
347 rawText : transformation.getTransformation(rawText, button).toString();
348 final int length = text.length();
349 mBreakIterator.setText(text);
350
351 if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
352 if (mBreakIterator.next() == BreakIterator.DONE) {
353 // Can't find a single possible line break in either direction.
354 return SQUEEZE_FAILED;
355 }
356 }
357
358 final TextPaint paint = button.getPaint();
359 final int initialPosition = mBreakIterator.current();
360 final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
361 final float initialRightTextWidth =
362 Layout.getDesiredWidth(text, initialPosition, length, paint);
363 float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
364
365 if (initialLeftTextWidth != initialRightTextWidth) {
366 // See if there's a better line-break point (leading to a more narrow button) in
367 // either left or right direction.
368 final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
369 final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
370 for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
371 final int newPosition =
372 moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
373 if (newPosition == BreakIterator.DONE) {
374 break;
375 }
376
377 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
378 final float newRightTextWidth =
379 Layout.getDesiredWidth(text, newPosition, length, paint);
380 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
381 if (newOptimalTextWidth < optimalTextWidth) {
382 optimalTextWidth = newOptimalTextWidth;
383 } else {
384 break;
385 }
386
387 boolean tooFar = moveLeft
388 ? newLeftTextWidth <= newRightTextWidth
389 : newLeftTextWidth >= newRightTextWidth;
390 if (tooFar) {
391 break;
392 }
393 }
394 }
395
396 return (int) Math.ceil(optimalTextWidth);
397 }
398
399 private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
400 int oldWidth = button.getMeasuredWidth();
401 if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
402 // Correct for the fact that the button was laid out with single-line horizontal
403 // padding.
404 oldWidth += mSingleToDoubleLineButtonWidthIncrease;
405 }
406
407 // Re-measure the squeezed smart reply button.
408 button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
409 mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
410 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
411 2 * mDoubleLineButtonPaddingHorizontal + textWidth, MeasureSpec.AT_MOST);
412 button.measure(widthMeasureSpec, heightMeasureSpec);
413
414 final int newWidth = button.getMeasuredWidth();
415
416 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
417 if (button.getLineCount() > 2 || newWidth >= oldWidth) {
418 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
419 return SQUEEZE_FAILED;
420 } else {
421 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
422 return oldWidth - newWidth;
423 }
424 }
425
Milo Sredkova5bacea2018-04-12 12:52:43 +0100426 private void remeasureButtonsIfNecessary(
Petr Cermak102431d2018-01-29 10:36:07 +0000427 int buttonPaddingHorizontal, int maxChildHeight) {
Petr Cermak102431d2018-01-29 10:36:07 +0000428 final int maxChildHeightMeasure =
429 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
430
431 final int childCount = getChildCount();
432 for (int i = 0; i < childCount; i++) {
433 final View child = getChildAt(i);
434 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
435 if (!lp.show) {
436 continue;
437 }
438
Petr Cermak102431d2018-01-29 10:36:07 +0000439 boolean requiresNewMeasure = false;
440 int newWidth = child.getMeasuredWidth();
441
442 // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
443 // in more than two lines or because it was unnecessary).
444 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
445 requiresNewMeasure = true;
446 newWidth = Integer.MAX_VALUE;
447 }
448
449 // Re-measure reason 2: The button's horizontal padding is incorrect (because it was
450 // measured with the wrong number of lines).
451 if (child.getPaddingLeft() != buttonPaddingHorizontal) {
452 requiresNewMeasure = true;
453 if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) {
454 // Decrease padding (2->1 line).
455 newWidth -= mSingleToDoubleLineButtonWidthIncrease;
456 } else {
457 // Increase padding (1->2 lines).
458 newWidth += mSingleToDoubleLineButtonWidthIncrease;
459 }
460 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
461 buttonPaddingHorizontal, child.getPaddingBottom());
462 }
463
464 // Re-measure reason 3: The button's height is less than the max height of all buttons
465 // (all should have the same height).
466 if (child.getMeasuredHeight() != maxChildHeight) {
467 requiresNewMeasure = true;
468 }
469
470 if (requiresNewMeasure) {
471 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
472 maxChildHeightMeasure);
473 }
474 }
475 }
476
477 private void markButtonsWithPendingSqueezeStatusAs(int squeezeStatus, int maxChildIndex) {
478 for (int i = 0; i <= maxChildIndex; i++) {
479 final View child = getChildAt(i);
480 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
481 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
482 lp.squeezeStatus = squeezeStatus;
483 }
484 }
485 }
486
487 @Override
488 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
489 final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
490
491 final int width = right - left;
492 int position = isRtl ? width - mPaddingRight : mPaddingLeft;
493
494 final int childCount = getChildCount();
495 for (int i = 0; i < childCount; i++) {
496 final View child = getChildAt(i);
497 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
498 if (!lp.show) {
499 continue;
500 }
501
502 final int childWidth = child.getMeasuredWidth();
503 final int childHeight = child.getMeasuredHeight();
504 final int childLeft = isRtl ? position - childWidth : position;
505 child.layout(childLeft, 0, childLeft + childWidth, childHeight);
506
507 final int childWidthWithSpacing = childWidth + mSpacing;
508 if (isRtl) {
509 position -= childWidthWithSpacing;
510 } else {
511 position += childWidthWithSpacing;
512 }
513 }
514 }
515
516 @Override
517 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
518 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
519 return lp.show && super.drawChild(canvas, child, drawingTime);
520 }
521
522 @VisibleForTesting
523 static class LayoutParams extends ViewGroup.LayoutParams {
524
525 /** Button is not squeezed. */
526 private static final int SQUEEZE_STATUS_NONE = 0;
527
528 /**
529 * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
530 * turns out to have been unnecessary (because there's still not enough space to add another
531 * button).
532 */
533 private static final int SQUEEZE_STATUS_PENDING = 1;
534
535 /** Button was successfully squeezed and it won't be un-squeezed. */
536 private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
537
538 /**
539 * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
540 * text or it didn't reduce the button's width at all. The button will have to be
541 * re-measured to use only one line of text.
542 */
543 private static final int SQUEEZE_STATUS_FAILED = 3;
544
545 private boolean show = false;
546 private int squeezeStatus = SQUEEZE_STATUS_NONE;
547
548 private LayoutParams(Context c, AttributeSet attrs) {
549 super(c, attrs);
550 }
551
552 private LayoutParams(int width, int height) {
553 super(width, height);
554 }
555
556 @VisibleForTesting
557 boolean isShown() {
558 return show;
559 }
560 }
Petr Cermaked7429c2017-12-18 19:38:04 +0000561}