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