blob: b4fa2e8b16f8250f02b92f420b95a64c94e0d0b2 [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 -> {
186 mKeyguardDismissUtil.dismissKeyguardThenExecute(
187 action, null /* cancelAction */, false /* afterKeyguardGone */);
Petr Cermaked7429c2017-12-18 19:38:04 +0000188 });
Milo Sredkov66da07b2018-04-17 14:04:54 +0100189
190 b.setAccessibilityDelegate(new AccessibilityDelegate() {
191 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
192 super.onInitializeAccessibilityNodeInfo(host, info);
193 String label = getResources().getString(R.string.accessibility_send_smart_reply);
194 info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
195 }
196 });
197
Petr Cermaked7429c2017-12-18 19:38:04 +0000198 return b;
199 }
Petr Cermak102431d2018-01-29 10:36:07 +0000200
201 @Override
202 public LayoutParams generateLayoutParams(AttributeSet attrs) {
203 return new LayoutParams(mContext, attrs);
204 }
205
206 @Override
207 protected LayoutParams generateDefaultLayoutParams() {
208 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
209 }
210
211 @Override
212 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
213 return new LayoutParams(params.width, params.height);
214 }
215
216 @Override
217 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
218 final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
219 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
220
221 // Mark all buttons as hidden and un-squeezed.
222 resetButtonsLayoutParams();
223
224 if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
225 Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
226 mCandidateButtonQueueForSqueezing.clear();
227 }
228
229 int measuredWidth = mPaddingLeft + mPaddingRight;
230 int maxChildHeight = 0;
231 int displayedChildCount = 0;
232 int buttonPaddingHorizontal = mSingleLineButtonPaddingHorizontal;
233
234 final int childCount = getChildCount();
235 for (int i = 0; i < childCount; i++) {
236 final View child = getChildAt(i);
237 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
238 if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
239 continue;
240 }
241
242 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
243 buttonPaddingHorizontal, child.getPaddingBottom());
244 child.measure(MEASURE_SPEC_ANY_WIDTH, heightMeasureSpec);
245
246 final int lineCount = ((Button) child).getLineCount();
247 if (lineCount < 1 || lineCount > 2) {
248 // If smart reply has no text, or more than two lines, then don't show it.
249 continue;
250 }
251
252 if (lineCount == 1) {
253 mCandidateButtonQueueForSqueezing.add((Button) child);
254 }
255
256 // Remember the current measurements in case the current button doesn't fit in.
257 final int originalMaxChildHeight = maxChildHeight;
258 final int originalMeasuredWidth = measuredWidth;
259 final int originalButtonPaddingHorizontal = buttonPaddingHorizontal;
260
261 final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
262 final int childWidth = child.getMeasuredWidth();
263 final int childHeight = child.getMeasuredHeight();
264 measuredWidth += spacing + childWidth;
265 maxChildHeight = Math.max(maxChildHeight, childHeight);
266
267 // Do we need to increase the number of lines in smart reply buttons to two?
268 final boolean increaseToTwoLines =
269 buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal
270 && (lineCount == 2 || measuredWidth > targetWidth);
271 if (increaseToTwoLines) {
272 measuredWidth += (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease;
273 buttonPaddingHorizontal = mDoubleLineButtonPaddingHorizontal;
274 }
275
276 // If the last button doesn't fit into the remaining width, try squeezing preceding
277 // smart reply buttons.
278 if (measuredWidth > targetWidth) {
279 // Keep squeezing preceding and current smart reply buttons until they all fit.
280 while (measuredWidth > targetWidth
281 && !mCandidateButtonQueueForSqueezing.isEmpty()) {
282 final Button candidate = mCandidateButtonQueueForSqueezing.poll();
283 final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
284 if (squeezeReduction != SQUEEZE_FAILED) {
285 maxChildHeight = Math.max(maxChildHeight, candidate.getMeasuredHeight());
286 measuredWidth -= squeezeReduction;
287 }
288 }
289
290 // If the current button still doesn't fit after squeezing all buttons, undo the
291 // last squeezing round.
292 if (measuredWidth > targetWidth) {
293 measuredWidth = originalMeasuredWidth;
294 maxChildHeight = originalMaxChildHeight;
295 buttonPaddingHorizontal = originalButtonPaddingHorizontal;
296
297 // Mark all buttons from the last squeezing round as "failed to squeeze", so
298 // that they're re-measured without squeezing later.
299 markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_FAILED, i);
300
301 // The current button doesn't fit, so there's no point in measuring further
302 // buttons.
303 break;
304 }
305
306 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
307 // to prevent them from being un-squeezed in a subsequent squeezing round.
308 markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, i);
309 }
310
311 lp.show = true;
312 displayedChildCount++;
313 }
314
315 // We're done squeezing buttons, so we can clear the priority queue.
316 mCandidateButtonQueueForSqueezing.clear();
317
Milo Sredkova5bacea2018-04-12 12:52:43 +0100318 // Finally, we need to re-measure some buttons.
319 remeasureButtonsIfNecessary(buttonPaddingHorizontal, maxChildHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000320
321 setMeasuredDimension(
322 resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec),
323 resolveSize(Math.max(getSuggestedMinimumHeight(),
324 mPaddingTop + maxChildHeight + mPaddingBottom), heightMeasureSpec));
325 }
326
327 private void resetButtonsLayoutParams() {
328 final int childCount = getChildCount();
329 for (int i = 0; i < childCount; i++) {
330 final View child = getChildAt(i);
331 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
332 lp.show = false;
333 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
334 }
335 }
336
337 private int squeezeButton(Button button, int heightMeasureSpec) {
338 final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
339 if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
340 return SQUEEZE_FAILED;
341 }
342 return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
343 }
344
345 private int estimateOptimalSqueezedButtonTextWidth(Button button) {
346 // Find a line-break point in the middle of the smart reply button text.
347 final String rawText = button.getText().toString();
348
349 // The button sometimes has a transformation affecting text layout (e.g. all caps).
350 final TransformationMethod transformation = button.getTransformationMethod();
351 final String text = transformation == null ?
352 rawText : transformation.getTransformation(rawText, button).toString();
353 final int length = text.length();
354 mBreakIterator.setText(text);
355
356 if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
357 if (mBreakIterator.next() == BreakIterator.DONE) {
358 // Can't find a single possible line break in either direction.
359 return SQUEEZE_FAILED;
360 }
361 }
362
363 final TextPaint paint = button.getPaint();
364 final int initialPosition = mBreakIterator.current();
365 final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
366 final float initialRightTextWidth =
367 Layout.getDesiredWidth(text, initialPosition, length, paint);
368 float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
369
370 if (initialLeftTextWidth != initialRightTextWidth) {
371 // See if there's a better line-break point (leading to a more narrow button) in
372 // either left or right direction.
373 final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
374 final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
375 for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
376 final int newPosition =
377 moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
378 if (newPosition == BreakIterator.DONE) {
379 break;
380 }
381
382 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
383 final float newRightTextWidth =
384 Layout.getDesiredWidth(text, newPosition, length, paint);
385 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
386 if (newOptimalTextWidth < optimalTextWidth) {
387 optimalTextWidth = newOptimalTextWidth;
388 } else {
389 break;
390 }
391
392 boolean tooFar = moveLeft
393 ? newLeftTextWidth <= newRightTextWidth
394 : newLeftTextWidth >= newRightTextWidth;
395 if (tooFar) {
396 break;
397 }
398 }
399 }
400
401 return (int) Math.ceil(optimalTextWidth);
402 }
403
404 private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
405 int oldWidth = button.getMeasuredWidth();
406 if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
407 // Correct for the fact that the button was laid out with single-line horizontal
408 // padding.
409 oldWidth += mSingleToDoubleLineButtonWidthIncrease;
410 }
411
412 // Re-measure the squeezed smart reply button.
413 button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
414 mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
415 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
416 2 * mDoubleLineButtonPaddingHorizontal + textWidth, MeasureSpec.AT_MOST);
417 button.measure(widthMeasureSpec, heightMeasureSpec);
418
419 final int newWidth = button.getMeasuredWidth();
420
421 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
422 if (button.getLineCount() > 2 || newWidth >= oldWidth) {
423 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
424 return SQUEEZE_FAILED;
425 } else {
426 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
427 return oldWidth - newWidth;
428 }
429 }
430
Milo Sredkova5bacea2018-04-12 12:52:43 +0100431 private void remeasureButtonsIfNecessary(
Petr Cermak102431d2018-01-29 10:36:07 +0000432 int buttonPaddingHorizontal, int maxChildHeight) {
Petr Cermak102431d2018-01-29 10:36:07 +0000433 final int maxChildHeightMeasure =
434 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
435
436 final int childCount = getChildCount();
437 for (int i = 0; i < childCount; i++) {
438 final View child = getChildAt(i);
439 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
440 if (!lp.show) {
441 continue;
442 }
443
Petr Cermak102431d2018-01-29 10:36:07 +0000444 boolean requiresNewMeasure = false;
445 int newWidth = child.getMeasuredWidth();
446
447 // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
448 // in more than two lines or because it was unnecessary).
449 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
450 requiresNewMeasure = true;
451 newWidth = Integer.MAX_VALUE;
452 }
453
454 // Re-measure reason 2: The button's horizontal padding is incorrect (because it was
455 // measured with the wrong number of lines).
456 if (child.getPaddingLeft() != buttonPaddingHorizontal) {
457 requiresNewMeasure = true;
458 if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) {
459 // Decrease padding (2->1 line).
460 newWidth -= mSingleToDoubleLineButtonWidthIncrease;
461 } else {
462 // Increase padding (1->2 lines).
463 newWidth += mSingleToDoubleLineButtonWidthIncrease;
464 }
465 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
466 buttonPaddingHorizontal, child.getPaddingBottom());
467 }
468
469 // Re-measure reason 3: The button's height is less than the max height of all buttons
470 // (all should have the same height).
471 if (child.getMeasuredHeight() != maxChildHeight) {
472 requiresNewMeasure = true;
473 }
474
475 if (requiresNewMeasure) {
476 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
477 maxChildHeightMeasure);
478 }
479 }
480 }
481
482 private void markButtonsWithPendingSqueezeStatusAs(int squeezeStatus, int maxChildIndex) {
483 for (int i = 0; i <= maxChildIndex; i++) {
484 final View child = getChildAt(i);
485 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
486 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
487 lp.squeezeStatus = squeezeStatus;
488 }
489 }
490 }
491
492 @Override
493 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
494 final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
495
496 final int width = right - left;
497 int position = isRtl ? width - mPaddingRight : mPaddingLeft;
498
499 final int childCount = getChildCount();
500 for (int i = 0; i < childCount; i++) {
501 final View child = getChildAt(i);
502 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
503 if (!lp.show) {
504 continue;
505 }
506
507 final int childWidth = child.getMeasuredWidth();
508 final int childHeight = child.getMeasuredHeight();
509 final int childLeft = isRtl ? position - childWidth : position;
510 child.layout(childLeft, 0, childLeft + childWidth, childHeight);
511
512 final int childWidthWithSpacing = childWidth + mSpacing;
513 if (isRtl) {
514 position -= childWidthWithSpacing;
515 } else {
516 position += childWidthWithSpacing;
517 }
518 }
519 }
520
521 @Override
522 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
523 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
524 return lp.show && super.drawChild(canvas, child, drawingTime);
525 }
526
527 @VisibleForTesting
528 static class LayoutParams extends ViewGroup.LayoutParams {
529
530 /** Button is not squeezed. */
531 private static final int SQUEEZE_STATUS_NONE = 0;
532
533 /**
534 * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
535 * turns out to have been unnecessary (because there's still not enough space to add another
536 * button).
537 */
538 private static final int SQUEEZE_STATUS_PENDING = 1;
539
540 /** Button was successfully squeezed and it won't be un-squeezed. */
541 private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
542
543 /**
544 * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
545 * text or it didn't reduce the button's width at all. The button will have to be
546 * re-measured to use only one line of text.
547 */
548 private static final int SQUEEZE_STATUS_FAILED = 3;
549
550 private boolean show = false;
551 private int squeezeStatus = SQUEEZE_STATUS_NONE;
552
553 private LayoutParams(Context c, AttributeSet attrs) {
554 super(c, attrs);
555 }
556
557 private LayoutParams(int width, int height) {
558 super(width, height);
559 }
560
561 @VisibleForTesting
562 boolean isShown() {
563 return show;
564 }
565 }
Petr Cermaked7429c2017-12-18 19:38:04 +0000566}