blob: 351868dd8b7b86721425270b4e3659bc331f9c54 [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;
Kenny Guya0f6de82018-04-06 16:20:16 +010029import com.android.systemui.statusbar.SmartReplyController;
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
Kenny Guya0f6de82018-04-06 16:20:16 +010076 private View mSmartReplyContainer;
77
Petr Cermaked7429c2017-12-18 19:38:04 +000078 public SmartReplyView(Context context, AttributeSet attrs) {
79 super(context, attrs);
Petr Cermak10011fa2018-02-05 19:00:54 +000080 mConstants = Dependency.get(SmartReplyConstants.class);
Milo Sredkovb0f55e92018-04-04 16:13:28 +010081 mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class);
Petr Cermak102431d2018-01-29 10:36:07 +000082
Milo Sredkove7cf4982018-04-09 15:08:26 +010083 mHeightUpperLimit = NotificationUtils.getFontScaledHeight(mContext,
84 R.dimen.smart_reply_button_max_height);
85
Petr Cermak102431d2018-01-29 10:36:07 +000086 int spacing = 0;
87 int singleLineButtonPaddingHorizontal = 0;
88 int doubleLineButtonPaddingHorizontal = 0;
89
90 final TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.SmartReplyView,
91 0, 0);
92 final int length = arr.getIndexCount();
93 for (int i = 0; i < length; i++) {
94 int attr = arr.getIndex(i);
95 switch (attr) {
96 case R.styleable.SmartReplyView_spacing:
97 spacing = arr.getDimensionPixelSize(i, 0);
98 break;
99 case R.styleable.SmartReplyView_singleLineButtonPaddingHorizontal:
100 singleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
101 break;
102 case R.styleable.SmartReplyView_doubleLineButtonPaddingHorizontal:
103 doubleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
104 break;
105 }
106 }
107 arr.recycle();
108
109 mSpacing = spacing;
110 mSingleLineButtonPaddingHorizontal = singleLineButtonPaddingHorizontal;
111 mDoubleLineButtonPaddingHorizontal = doubleLineButtonPaddingHorizontal;
112 mSingleToDoubleLineButtonWidthIncrease =
113 2 * (doubleLineButtonPaddingHorizontal - singleLineButtonPaddingHorizontal);
114
Milo Sredkove7cf4982018-04-09 15:08:26 +0100115
Petr Cermak102431d2018-01-29 10:36:07 +0000116 mBreakIterator = BreakIterator.getLineInstance();
117 reallocateCandidateButtonQueueForSqueezing();
118 }
119
Milo Sredkove7cf4982018-04-09 15:08:26 +0100120 /**
121 * Returns an upper bound for the height of this view in pixels. This method is intended to be
122 * invoked before onMeasure, so it doesn't do any analysis on the contents of the buttons.
123 */
124 public int getHeightUpperLimit() {
125 return mHeightUpperLimit;
126 }
127
Petr Cermak102431d2018-01-29 10:36:07 +0000128 private void reallocateCandidateButtonQueueForSqueezing() {
129 // Instead of clearing the priority queue, we re-allocate so that it would fit all buttons
130 // exactly. This avoids (1) wasting memory because PriorityQueue never shrinks and
131 // (2) growing in onMeasure.
132 // The constructor throws an IllegalArgument exception if initial capacity is less than 1.
133 mCandidateButtonQueueForSqueezing = new PriorityQueue<>(
134 Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
Petr Cermaked7429c2017-12-18 19:38:04 +0000135 }
136
Kenny Guy23991102018-04-05 21:18:38 +0100137 public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent,
Kenny Guya0f6de82018-04-06 16:20:16 +0100138 SmartReplyController smartReplyController, NotificationData.Entry entry,
139 View smartReplyContainer) {
140 mSmartReplyContainer = smartReplyContainer;
Petr Cermaked7429c2017-12-18 19:38:04 +0000141 removeAllViews();
142 if (remoteInput != null && pendingIntent != null) {
143 CharSequence[] choices = remoteInput.getChoices();
144 if (choices != null) {
Kenny Guy23991102018-04-05 21:18:38 +0100145 for (int i = 0; i < choices.length; ++i) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000146 Button replyButton = inflateReplyButton(
Kenny Guy23991102018-04-05 21:18:38 +0100147 getContext(), this, i, choices[i], remoteInput, pendingIntent,
Kenny Guya0f6de82018-04-06 16:20:16 +0100148 smartReplyController, entry);
Petr Cermaked7429c2017-12-18 19:38:04 +0000149 addView(replyButton);
150 }
151 }
152 }
Petr Cermak102431d2018-01-29 10:36:07 +0000153 reallocateCandidateButtonQueueForSqueezing();
Petr Cermaked7429c2017-12-18 19:38:04 +0000154 }
155
156 public static SmartReplyView inflate(Context context, ViewGroup root) {
157 return (SmartReplyView)
158 LayoutInflater.from(context).inflate(R.layout.smart_reply_view, root, false);
159 }
160
Petr Cermak102431d2018-01-29 10:36:07 +0000161 @VisibleForTesting
Kenny Guy23991102018-04-05 21:18:38 +0100162 Button inflateReplyButton(Context context, ViewGroup root, int replyIndex,
163 CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent,
Kenny Guya0f6de82018-04-06 16:20:16 +0100164 SmartReplyController smartReplyController, NotificationData.Entry entry) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000165 Button b = (Button) LayoutInflater.from(context).inflate(
166 R.layout.smart_reply_button, root, false);
167 b.setText(choice);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100168
169 OnDismissAction action = () -> {
Petr Cermaked7429c2017-12-18 19:38:04 +0000170 Bundle results = new Bundle();
171 results.putString(remoteInput.getResultKey(), choice.toString());
172 Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
173 RemoteInput.addResultsToIntent(new RemoteInput[]{remoteInput}, intent, results);
Petr Cermak9a3380c2018-01-19 15:00:24 +0000174 RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
Petr Cermaked7429c2017-12-18 19:38:04 +0000175 try {
176 pendingIntent.send(context, 0, intent);
177 } catch (PendingIntent.CanceledException e) {
178 Log.w(TAG, "Unable to send smart reply", e);
179 }
Kenny Guya0f6de82018-04-06 16:20:16 +0100180 smartReplyController.smartReplySent(entry, replyIndex, b.getText());
181 mSmartReplyContainer.setVisibility(View.GONE);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100182 return false; // do not defer
183 };
184
185 b.setOnClickListener(view -> {
Milo Sredkove433e9b2018-05-01 22:45:38 +0100186 mKeyguardDismissUtil.executeWhenUnlocked(action);
Petr Cermaked7429c2017-12-18 19:38:04 +0000187 });
Milo Sredkov66da07b2018-04-17 14:04:54 +0100188
189 b.setAccessibilityDelegate(new AccessibilityDelegate() {
190 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
191 super.onInitializeAccessibilityNodeInfo(host, info);
192 String label = getResources().getString(R.string.accessibility_send_smart_reply);
193 info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
194 }
195 });
196
Petr Cermaked7429c2017-12-18 19:38:04 +0000197 return b;
198 }
Petr Cermak102431d2018-01-29 10:36:07 +0000199
200 @Override
201 public LayoutParams generateLayoutParams(AttributeSet attrs) {
202 return new LayoutParams(mContext, attrs);
203 }
204
205 @Override
206 protected LayoutParams generateDefaultLayoutParams() {
207 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
208 }
209
210 @Override
211 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
212 return new LayoutParams(params.width, params.height);
213 }
214
215 @Override
216 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
217 final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
218 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
219
220 // Mark all buttons as hidden and un-squeezed.
221 resetButtonsLayoutParams();
222
223 if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
224 Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
225 mCandidateButtonQueueForSqueezing.clear();
226 }
227
228 int measuredWidth = mPaddingLeft + mPaddingRight;
229 int maxChildHeight = 0;
230 int displayedChildCount = 0;
231 int buttonPaddingHorizontal = mSingleLineButtonPaddingHorizontal;
232
233 final int childCount = getChildCount();
234 for (int i = 0; i < childCount; i++) {
235 final View child = getChildAt(i);
236 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
237 if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
238 continue;
239 }
240
241 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
242 buttonPaddingHorizontal, child.getPaddingBottom());
243 child.measure(MEASURE_SPEC_ANY_WIDTH, heightMeasureSpec);
244
245 final int lineCount = ((Button) child).getLineCount();
246 if (lineCount < 1 || lineCount > 2) {
247 // If smart reply has no text, or more than two lines, then don't show it.
248 continue;
249 }
250
251 if (lineCount == 1) {
252 mCandidateButtonQueueForSqueezing.add((Button) child);
253 }
254
255 // Remember the current measurements in case the current button doesn't fit in.
256 final int originalMaxChildHeight = maxChildHeight;
257 final int originalMeasuredWidth = measuredWidth;
258 final int originalButtonPaddingHorizontal = buttonPaddingHorizontal;
259
260 final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
261 final int childWidth = child.getMeasuredWidth();
262 final int childHeight = child.getMeasuredHeight();
263 measuredWidth += spacing + childWidth;
264 maxChildHeight = Math.max(maxChildHeight, childHeight);
265
266 // Do we need to increase the number of lines in smart reply buttons to two?
267 final boolean increaseToTwoLines =
268 buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal
269 && (lineCount == 2 || measuredWidth > targetWidth);
270 if (increaseToTwoLines) {
271 measuredWidth += (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease;
272 buttonPaddingHorizontal = mDoubleLineButtonPaddingHorizontal;
273 }
274
275 // If the last button doesn't fit into the remaining width, try squeezing preceding
276 // smart reply buttons.
277 if (measuredWidth > targetWidth) {
278 // Keep squeezing preceding and current smart reply buttons until they all fit.
279 while (measuredWidth > targetWidth
280 && !mCandidateButtonQueueForSqueezing.isEmpty()) {
281 final Button candidate = mCandidateButtonQueueForSqueezing.poll();
282 final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
283 if (squeezeReduction != SQUEEZE_FAILED) {
284 maxChildHeight = Math.max(maxChildHeight, candidate.getMeasuredHeight());
285 measuredWidth -= squeezeReduction;
286 }
287 }
288
289 // If the current button still doesn't fit after squeezing all buttons, undo the
290 // last squeezing round.
291 if (measuredWidth > targetWidth) {
292 measuredWidth = originalMeasuredWidth;
293 maxChildHeight = originalMaxChildHeight;
294 buttonPaddingHorizontal = originalButtonPaddingHorizontal;
295
296 // Mark all buttons from the last squeezing round as "failed to squeeze", so
297 // that they're re-measured without squeezing later.
298 markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_FAILED, i);
299
300 // The current button doesn't fit, so there's no point in measuring further
301 // buttons.
302 break;
303 }
304
305 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
306 // to prevent them from being un-squeezed in a subsequent squeezing round.
307 markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, i);
308 }
309
310 lp.show = true;
311 displayedChildCount++;
312 }
313
314 // We're done squeezing buttons, so we can clear the priority queue.
315 mCandidateButtonQueueForSqueezing.clear();
316
Milo Sredkova5bacea2018-04-12 12:52:43 +0100317 // Finally, we need to re-measure some buttons.
318 remeasureButtonsIfNecessary(buttonPaddingHorizontal, maxChildHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000319
320 setMeasuredDimension(
321 resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec),
322 resolveSize(Math.max(getSuggestedMinimumHeight(),
323 mPaddingTop + maxChildHeight + mPaddingBottom), heightMeasureSpec));
324 }
325
326 private void resetButtonsLayoutParams() {
327 final int childCount = getChildCount();
328 for (int i = 0; i < childCount; i++) {
329 final View child = getChildAt(i);
330 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
331 lp.show = false;
332 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
333 }
334 }
335
336 private int squeezeButton(Button button, int heightMeasureSpec) {
337 final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
338 if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
339 return SQUEEZE_FAILED;
340 }
341 return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
342 }
343
344 private int estimateOptimalSqueezedButtonTextWidth(Button button) {
345 // Find a line-break point in the middle of the smart reply button text.
346 final String rawText = button.getText().toString();
347
348 // The button sometimes has a transformation affecting text layout (e.g. all caps).
349 final TransformationMethod transformation = button.getTransformationMethod();
350 final String text = transformation == null ?
351 rawText : transformation.getTransformation(rawText, button).toString();
352 final int length = text.length();
353 mBreakIterator.setText(text);
354
355 if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
356 if (mBreakIterator.next() == BreakIterator.DONE) {
357 // Can't find a single possible line break in either direction.
358 return SQUEEZE_FAILED;
359 }
360 }
361
362 final TextPaint paint = button.getPaint();
363 final int initialPosition = mBreakIterator.current();
364 final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
365 final float initialRightTextWidth =
366 Layout.getDesiredWidth(text, initialPosition, length, paint);
367 float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
368
369 if (initialLeftTextWidth != initialRightTextWidth) {
370 // See if there's a better line-break point (leading to a more narrow button) in
371 // either left or right direction.
372 final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
373 final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
374 for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
375 final int newPosition =
376 moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
377 if (newPosition == BreakIterator.DONE) {
378 break;
379 }
380
381 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
382 final float newRightTextWidth =
383 Layout.getDesiredWidth(text, newPosition, length, paint);
384 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
385 if (newOptimalTextWidth < optimalTextWidth) {
386 optimalTextWidth = newOptimalTextWidth;
387 } else {
388 break;
389 }
390
391 boolean tooFar = moveLeft
392 ? newLeftTextWidth <= newRightTextWidth
393 : newLeftTextWidth >= newRightTextWidth;
394 if (tooFar) {
395 break;
396 }
397 }
398 }
399
400 return (int) Math.ceil(optimalTextWidth);
401 }
402
403 private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
404 int oldWidth = button.getMeasuredWidth();
405 if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
406 // Correct for the fact that the button was laid out with single-line horizontal
407 // padding.
408 oldWidth += mSingleToDoubleLineButtonWidthIncrease;
409 }
410
411 // Re-measure the squeezed smart reply button.
412 button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
413 mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
414 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
415 2 * mDoubleLineButtonPaddingHorizontal + textWidth, MeasureSpec.AT_MOST);
416 button.measure(widthMeasureSpec, heightMeasureSpec);
417
418 final int newWidth = button.getMeasuredWidth();
419
420 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
421 if (button.getLineCount() > 2 || newWidth >= oldWidth) {
422 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
423 return SQUEEZE_FAILED;
424 } else {
425 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
426 return oldWidth - newWidth;
427 }
428 }
429
Milo Sredkova5bacea2018-04-12 12:52:43 +0100430 private void remeasureButtonsIfNecessary(
Petr Cermak102431d2018-01-29 10:36:07 +0000431 int buttonPaddingHorizontal, int maxChildHeight) {
Petr Cermak102431d2018-01-29 10:36:07 +0000432 final int maxChildHeightMeasure =
433 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
434
435 final int childCount = getChildCount();
436 for (int i = 0; i < childCount; i++) {
437 final View child = getChildAt(i);
438 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
439 if (!lp.show) {
440 continue;
441 }
442
Petr Cermak102431d2018-01-29 10:36:07 +0000443 boolean requiresNewMeasure = false;
444 int newWidth = child.getMeasuredWidth();
445
446 // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
447 // in more than two lines or because it was unnecessary).
448 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
449 requiresNewMeasure = true;
450 newWidth = Integer.MAX_VALUE;
451 }
452
453 // Re-measure reason 2: The button's horizontal padding is incorrect (because it was
454 // measured with the wrong number of lines).
455 if (child.getPaddingLeft() != buttonPaddingHorizontal) {
456 requiresNewMeasure = true;
457 if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) {
458 // Decrease padding (2->1 line).
459 newWidth -= mSingleToDoubleLineButtonWidthIncrease;
460 } else {
461 // Increase padding (1->2 lines).
462 newWidth += mSingleToDoubleLineButtonWidthIncrease;
463 }
464 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
465 buttonPaddingHorizontal, child.getPaddingBottom());
466 }
467
468 // Re-measure reason 3: The button's height is less than the max height of all buttons
469 // (all should have the same height).
470 if (child.getMeasuredHeight() != maxChildHeight) {
471 requiresNewMeasure = true;
472 }
473
474 if (requiresNewMeasure) {
475 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
476 maxChildHeightMeasure);
477 }
478 }
479 }
480
481 private void markButtonsWithPendingSqueezeStatusAs(int squeezeStatus, int maxChildIndex) {
482 for (int i = 0; i <= maxChildIndex; i++) {
483 final View child = getChildAt(i);
484 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
485 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
486 lp.squeezeStatus = squeezeStatus;
487 }
488 }
489 }
490
491 @Override
492 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
493 final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
494
495 final int width = right - left;
496 int position = isRtl ? width - mPaddingRight : mPaddingLeft;
497
498 final int childCount = getChildCount();
499 for (int i = 0; i < childCount; i++) {
500 final View child = getChildAt(i);
501 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
502 if (!lp.show) {
503 continue;
504 }
505
506 final int childWidth = child.getMeasuredWidth();
507 final int childHeight = child.getMeasuredHeight();
508 final int childLeft = isRtl ? position - childWidth : position;
509 child.layout(childLeft, 0, childLeft + childWidth, childHeight);
510
511 final int childWidthWithSpacing = childWidth + mSpacing;
512 if (isRtl) {
513 position -= childWidthWithSpacing;
514 } else {
515 position += childWidthWithSpacing;
516 }
517 }
518 }
519
520 @Override
521 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
522 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
523 return lp.show && super.drawChild(canvas, child, drawingTime);
524 }
525
526 @VisibleForTesting
527 static class LayoutParams extends ViewGroup.LayoutParams {
528
529 /** Button is not squeezed. */
530 private static final int SQUEEZE_STATUS_NONE = 0;
531
532 /**
533 * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
534 * turns out to have been unnecessary (because there's still not enough space to add another
535 * button).
536 */
537 private static final int SQUEEZE_STATUS_PENDING = 1;
538
539 /** Button was successfully squeezed and it won't be un-squeezed. */
540 private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
541
542 /**
543 * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
544 * text or it didn't reduce the button's width at all. The button will have to be
545 * re-measured to use only one line of text.
546 */
547 private static final int SQUEEZE_STATUS_FAILED = 3;
548
549 private boolean show = false;
550 private int squeezeStatus = SQUEEZE_STATUS_NONE;
551
552 private LayoutParams(Context c, AttributeSet attrs) {
553 super(c, attrs);
554 }
555
556 private LayoutParams(int width, int height) {
557 super(width, height);
558 }
559
560 @VisibleForTesting
561 boolean isShown() {
562 return show;
563 }
564 }
Petr Cermaked7429c2017-12-18 19:38:04 +0000565}