blob: dd031623f356fc568bdc006fcd4b35229a0155b7 [file] [log] [blame]
Petr Cermaked7429c2017-12-18 19:38:04 +00001package com.android.systemui.statusbar.policy;
2
Kenny Guy14d035c2018-05-02 19:10:36 +01003import android.annotation.ColorInt;
Petr Cermaked7429c2017-12-18 19:38:04 +00004import android.app.PendingIntent;
5import android.app.RemoteInput;
6import android.content.Context;
7import android.content.Intent;
Kenny Guy14d035c2018-05-02 19:10:36 +01008import android.content.res.ColorStateList;
Petr Cermak102431d2018-01-29 10:36:07 +00009import android.content.res.TypedArray;
10import android.graphics.Canvas;
Kenny Guy14d035c2018-05-02 19:10:36 +010011import android.graphics.Color;
12import android.graphics.drawable.Drawable;
Petr Cermak102431d2018-01-29 10:36:07 +000013import android.graphics.drawable.GradientDrawable;
Kenny Guy14d035c2018-05-02 19:10:36 +010014import android.graphics.drawable.InsetDrawable;
Petr Cermak102431d2018-01-29 10:36:07 +000015import android.graphics.drawable.RippleDrawable;
Petr Cermaked7429c2017-12-18 19:38:04 +000016import android.os.Bundle;
Petr Cermak102431d2018-01-29 10:36:07 +000017import android.text.Layout;
18import android.text.TextPaint;
19import android.text.method.TransformationMethod;
Petr Cermaked7429c2017-12-18 19:38:04 +000020import android.util.AttributeSet;
21import android.util.Log;
22import android.view.LayoutInflater;
Petr Cermak102431d2018-01-29 10:36:07 +000023import android.view.View;
Petr Cermaked7429c2017-12-18 19:38:04 +000024import android.view.ViewGroup;
Milo Sredkov66da07b2018-04-17 14:04:54 +010025import android.view.accessibility.AccessibilityNodeInfo;
26import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Petr Cermaked7429c2017-12-18 19:38:04 +000027import android.widget.Button;
Petr Cermaked7429c2017-12-18 19:38:04 +000028
Petr Cermak102431d2018-01-29 10:36:07 +000029import com.android.internal.annotations.VisibleForTesting;
Lucas Dupina291d192018-06-07 13:59:42 -070030import com.android.internal.util.ContrastColorUtil;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010031import com.android.keyguard.KeyguardHostView.OnDismissAction;
Petr Cermak10011fa2018-02-05 19:00:54 +000032import com.android.systemui.Dependency;
Petr Cermaked7429c2017-12-18 19:38:04 +000033import com.android.systemui.R;
Rohan Shah20790b82018-07-02 17:21:04 -070034import com.android.systemui.statusbar.notification.NotificationData;
Kenny Guya0f6de82018-04-06 16:20:16 +010035import com.android.systemui.statusbar.SmartReplyController;
Milo Sredkove7cf4982018-04-09 15:08:26 +010036import com.android.systemui.statusbar.notification.NotificationUtils;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010037import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
Petr Cermaked7429c2017-12-18 19:38:04 +000038
Petr Cermak102431d2018-01-29 10:36:07 +000039import java.text.BreakIterator;
40import java.util.Comparator;
41import java.util.PriorityQueue;
42
Petr Cermaked7429c2017-12-18 19:38:04 +000043/** View which displays smart reply buttons in notifications. */
Petr Cermak102431d2018-01-29 10:36:07 +000044public class SmartReplyView extends ViewGroup {
Petr Cermaked7429c2017-12-18 19:38:04 +000045
46 private static final String TAG = "SmartReplyView";
47
Petr Cermak102431d2018-01-29 10:36:07 +000048 private static final int MEASURE_SPEC_ANY_WIDTH =
49 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
50
51 private static final Comparator<View> DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR =
52 (v1, v2) -> ((v2.getMeasuredWidth() - v2.getPaddingLeft() - v2.getPaddingRight())
53 - (v1.getMeasuredWidth() - v1.getPaddingLeft() - v1.getPaddingRight()));
54
55 private static final int SQUEEZE_FAILED = -1;
56
Petr Cermak10011fa2018-02-05 19:00:54 +000057 private final SmartReplyConstants mConstants;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010058 private final KeyguardDismissUtil mKeyguardDismissUtil;
Petr Cermak10011fa2018-02-05 19:00:54 +000059
Milo Sredkove7cf4982018-04-09 15:08:26 +010060 /**
61 * The upper bound for the height of this view in pixels. Notifications are automatically
62 * recreated on density or font size changes so caching this should be fine.
63 */
64 private final int mHeightUpperLimit;
65
Petr Cermak102431d2018-01-29 10:36:07 +000066 /** Spacing to be applied between views. */
67 private final int mSpacing;
68
69 /** Horizontal padding of smart reply buttons if all of them use only one line of text. */
70 private final int mSingleLineButtonPaddingHorizontal;
71
72 /** Horizontal padding of smart reply buttons if at least one of them uses two lines of text. */
73 private final int mDoubleLineButtonPaddingHorizontal;
74
75 /** Increase in width of a smart reply button as a result of using two lines instead of one. */
76 private final int mSingleToDoubleLineButtonWidthIncrease;
77
78 private final BreakIterator mBreakIterator;
79
80 private PriorityQueue<Button> mCandidateButtonQueueForSqueezing;
81
Kenny Guya0f6de82018-04-06 16:20:16 +010082 private View mSmartReplyContainer;
83
Kenny Guy14d035c2018-05-02 19:10:36 +010084 @ColorInt
85 private int mCurrentBackgroundColor;
86 @ColorInt
87 private final int mDefaultBackgroundColor;
88 @ColorInt
89 private final int mDefaultStrokeColor;
90 @ColorInt
91 private final int mDefaultTextColor;
92 @ColorInt
93 private final int mDefaultTextColorDarkBg;
94 @ColorInt
95 private final int mRippleColorDarkBg;
96 @ColorInt
97 private final int mRippleColor;
98 private final int mStrokeWidth;
99 private final double mMinStrokeContrast;
100
Petr Cermaked7429c2017-12-18 19:38:04 +0000101 public SmartReplyView(Context context, AttributeSet attrs) {
102 super(context, attrs);
Petr Cermak10011fa2018-02-05 19:00:54 +0000103 mConstants = Dependency.get(SmartReplyConstants.class);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100104 mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class);
Petr Cermak102431d2018-01-29 10:36:07 +0000105
Milo Sredkove7cf4982018-04-09 15:08:26 +0100106 mHeightUpperLimit = NotificationUtils.getFontScaledHeight(mContext,
107 R.dimen.smart_reply_button_max_height);
108
Kenny Guy14d035c2018-05-02 19:10:36 +0100109 mCurrentBackgroundColor = context.getColor(R.color.smart_reply_button_background);
110 mDefaultBackgroundColor = mCurrentBackgroundColor;
111 mDefaultTextColor = mContext.getColor(R.color.smart_reply_button_text);
112 mDefaultTextColorDarkBg = mContext.getColor(R.color.smart_reply_button_text_dark_bg);
113 mDefaultStrokeColor = mContext.getColor(R.color.smart_reply_button_stroke);
114 mRippleColor = mContext.getColor(R.color.notification_ripple_untinted_color);
115 mRippleColorDarkBg = Color.argb(Color.alpha(mRippleColor),
116 255 /* red */, 255 /* green */, 255 /* blue */);
Lucas Dupina291d192018-06-07 13:59:42 -0700117 mMinStrokeContrast = ContrastColorUtil.calculateContrast(mDefaultStrokeColor,
Kenny Guy14d035c2018-05-02 19:10:36 +0100118 mDefaultBackgroundColor);
119
Petr Cermak102431d2018-01-29 10:36:07 +0000120 int spacing = 0;
121 int singleLineButtonPaddingHorizontal = 0;
122 int doubleLineButtonPaddingHorizontal = 0;
Kenny Guy14d035c2018-05-02 19:10:36 +0100123 int strokeWidth = 0;
Petr Cermak102431d2018-01-29 10:36:07 +0000124
125 final TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.SmartReplyView,
126 0, 0);
127 final int length = arr.getIndexCount();
128 for (int i = 0; i < length; i++) {
129 int attr = arr.getIndex(i);
Jason Monk05dd5672018-08-09 09:38:21 -0400130 if (attr == R.styleable.SmartReplyView_spacing) {
131 spacing = arr.getDimensionPixelSize(i, 0);
132 } else if (attr == R.styleable.SmartReplyView_singleLineButtonPaddingHorizontal) {
133 singleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
134 } else if (attr == R.styleable.SmartReplyView_doubleLineButtonPaddingHorizontal) {
135 doubleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
136 } else if (attr == R.styleable.SmartReplyView_buttonStrokeWidth) {
137 strokeWidth = arr.getDimensionPixelSize(i, 0);
Petr Cermak102431d2018-01-29 10:36:07 +0000138 }
139 }
140 arr.recycle();
141
Kenny Guy14d035c2018-05-02 19:10:36 +0100142 mStrokeWidth = strokeWidth;
Petr Cermak102431d2018-01-29 10:36:07 +0000143 mSpacing = spacing;
144 mSingleLineButtonPaddingHorizontal = singleLineButtonPaddingHorizontal;
145 mDoubleLineButtonPaddingHorizontal = doubleLineButtonPaddingHorizontal;
146 mSingleToDoubleLineButtonWidthIncrease =
147 2 * (doubleLineButtonPaddingHorizontal - singleLineButtonPaddingHorizontal);
148
Milo Sredkove7cf4982018-04-09 15:08:26 +0100149
Petr Cermak102431d2018-01-29 10:36:07 +0000150 mBreakIterator = BreakIterator.getLineInstance();
151 reallocateCandidateButtonQueueForSqueezing();
152 }
153
Milo Sredkove7cf4982018-04-09 15:08:26 +0100154 /**
155 * Returns an upper bound for the height of this view in pixels. This method is intended to be
156 * invoked before onMeasure, so it doesn't do any analysis on the contents of the buttons.
157 */
158 public int getHeightUpperLimit() {
159 return mHeightUpperLimit;
160 }
161
Petr Cermak102431d2018-01-29 10:36:07 +0000162 private void reallocateCandidateButtonQueueForSqueezing() {
163 // Instead of clearing the priority queue, we re-allocate so that it would fit all buttons
164 // exactly. This avoids (1) wasting memory because PriorityQueue never shrinks and
165 // (2) growing in onMeasure.
166 // The constructor throws an IllegalArgument exception if initial capacity is less than 1.
167 mCandidateButtonQueueForSqueezing = new PriorityQueue<>(
168 Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
Petr Cermaked7429c2017-12-18 19:38:04 +0000169 }
170
Tony Makc9acf672018-07-20 13:58:24 +0200171 public void setRepliesFromRemoteInput(
172 RemoteInput remoteInput, PendingIntent pendingIntent,
Kenny Guya0f6de82018-04-06 16:20:16 +0100173 SmartReplyController smartReplyController, NotificationData.Entry entry,
Tony Makc9acf672018-07-20 13:58:24 +0200174 View smartReplyContainer, CharSequence[] choices) {
Kenny Guya0f6de82018-04-06 16:20:16 +0100175 mSmartReplyContainer = smartReplyContainer;
Petr Cermaked7429c2017-12-18 19:38:04 +0000176 removeAllViews();
Kenny Guy14d035c2018-05-02 19:10:36 +0100177 mCurrentBackgroundColor = mDefaultBackgroundColor;
Petr Cermaked7429c2017-12-18 19:38:04 +0000178 if (remoteInput != null && pendingIntent != null) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000179 if (choices != null) {
Kenny Guy23991102018-04-05 21:18:38 +0100180 for (int i = 0; i < choices.length; ++i) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000181 Button replyButton = inflateReplyButton(
Kenny Guy23991102018-04-05 21:18:38 +0100182 getContext(), this, i, choices[i], remoteInput, pendingIntent,
Kenny Guya0f6de82018-04-06 16:20:16 +0100183 smartReplyController, entry);
Petr Cermaked7429c2017-12-18 19:38:04 +0000184 addView(replyButton);
185 }
186 }
187 }
Petr Cermak102431d2018-01-29 10:36:07 +0000188 reallocateCandidateButtonQueueForSqueezing();
Petr Cermaked7429c2017-12-18 19:38:04 +0000189 }
190
191 public static SmartReplyView inflate(Context context, ViewGroup root) {
192 return (SmartReplyView)
193 LayoutInflater.from(context).inflate(R.layout.smart_reply_view, root, false);
194 }
195
Petr Cermak102431d2018-01-29 10:36:07 +0000196 @VisibleForTesting
Kenny Guy23991102018-04-05 21:18:38 +0100197 Button inflateReplyButton(Context context, ViewGroup root, int replyIndex,
198 CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent,
Kenny Guya0f6de82018-04-06 16:20:16 +0100199 SmartReplyController smartReplyController, NotificationData.Entry entry) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000200 Button b = (Button) LayoutInflater.from(context).inflate(
201 R.layout.smart_reply_button, root, false);
202 b.setText(choice);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100203
204 OnDismissAction action = () -> {
Kenny Guy8cc15d22018-05-09 09:50:55 +0100205 smartReplyController.smartReplySent(entry, replyIndex, b.getText());
Petr Cermaked7429c2017-12-18 19:38:04 +0000206 Bundle results = new Bundle();
207 results.putString(remoteInput.getResultKey(), choice.toString());
208 Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
209 RemoteInput.addResultsToIntent(new RemoteInput[]{remoteInput}, intent, results);
Petr Cermak9a3380c2018-01-19 15:00:24 +0000210 RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
Selim Cinekb0dc61b2018-05-22 18:49:36 -0700211 entry.setHasSentReply();
Petr Cermaked7429c2017-12-18 19:38:04 +0000212 try {
213 pendingIntent.send(context, 0, intent);
214 } catch (PendingIntent.CanceledException e) {
215 Log.w(TAG, "Unable to send smart reply", e);
216 }
Kenny Guya0f6de82018-04-06 16:20:16 +0100217 mSmartReplyContainer.setVisibility(View.GONE);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100218 return false; // do not defer
219 };
220
221 b.setOnClickListener(view -> {
Milo Sredkove433e9b2018-05-01 22:45:38 +0100222 mKeyguardDismissUtil.executeWhenUnlocked(action);
Petr Cermaked7429c2017-12-18 19:38:04 +0000223 });
Milo Sredkov66da07b2018-04-17 14:04:54 +0100224
225 b.setAccessibilityDelegate(new AccessibilityDelegate() {
226 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
227 super.onInitializeAccessibilityNodeInfo(host, info);
228 String label = getResources().getString(R.string.accessibility_send_smart_reply);
229 info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
230 }
231 });
232
Kenny Guy14d035c2018-05-02 19:10:36 +0100233 setColors(b, mCurrentBackgroundColor, mDefaultStrokeColor, mDefaultTextColor, mRippleColor);
Petr Cermaked7429c2017-12-18 19:38:04 +0000234 return b;
235 }
Petr Cermak102431d2018-01-29 10:36:07 +0000236
237 @Override
238 public LayoutParams generateLayoutParams(AttributeSet attrs) {
239 return new LayoutParams(mContext, attrs);
240 }
241
242 @Override
243 protected LayoutParams generateDefaultLayoutParams() {
244 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
245 }
246
247 @Override
248 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
249 return new LayoutParams(params.width, params.height);
250 }
251
252 @Override
253 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
254 final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
255 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
256
257 // Mark all buttons as hidden and un-squeezed.
258 resetButtonsLayoutParams();
259
260 if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
261 Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
262 mCandidateButtonQueueForSqueezing.clear();
263 }
264
265 int measuredWidth = mPaddingLeft + mPaddingRight;
266 int maxChildHeight = 0;
267 int displayedChildCount = 0;
268 int buttonPaddingHorizontal = mSingleLineButtonPaddingHorizontal;
269
270 final int childCount = getChildCount();
271 for (int i = 0; i < childCount; i++) {
272 final View child = getChildAt(i);
273 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
274 if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
275 continue;
276 }
277
278 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
279 buttonPaddingHorizontal, child.getPaddingBottom());
280 child.measure(MEASURE_SPEC_ANY_WIDTH, heightMeasureSpec);
281
282 final int lineCount = ((Button) child).getLineCount();
283 if (lineCount < 1 || lineCount > 2) {
284 // If smart reply has no text, or more than two lines, then don't show it.
285 continue;
286 }
287
288 if (lineCount == 1) {
289 mCandidateButtonQueueForSqueezing.add((Button) child);
290 }
291
292 // Remember the current measurements in case the current button doesn't fit in.
293 final int originalMaxChildHeight = maxChildHeight;
294 final int originalMeasuredWidth = measuredWidth;
295 final int originalButtonPaddingHorizontal = buttonPaddingHorizontal;
296
297 final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
298 final int childWidth = child.getMeasuredWidth();
299 final int childHeight = child.getMeasuredHeight();
300 measuredWidth += spacing + childWidth;
301 maxChildHeight = Math.max(maxChildHeight, childHeight);
302
303 // Do we need to increase the number of lines in smart reply buttons to two?
304 final boolean increaseToTwoLines =
305 buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal
306 && (lineCount == 2 || measuredWidth > targetWidth);
307 if (increaseToTwoLines) {
308 measuredWidth += (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease;
309 buttonPaddingHorizontal = mDoubleLineButtonPaddingHorizontal;
310 }
311
312 // If the last button doesn't fit into the remaining width, try squeezing preceding
313 // smart reply buttons.
314 if (measuredWidth > targetWidth) {
315 // Keep squeezing preceding and current smart reply buttons until they all fit.
316 while (measuredWidth > targetWidth
317 && !mCandidateButtonQueueForSqueezing.isEmpty()) {
318 final Button candidate = mCandidateButtonQueueForSqueezing.poll();
319 final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
320 if (squeezeReduction != SQUEEZE_FAILED) {
321 maxChildHeight = Math.max(maxChildHeight, candidate.getMeasuredHeight());
322 measuredWidth -= squeezeReduction;
323 }
324 }
325
326 // If the current button still doesn't fit after squeezing all buttons, undo the
327 // last squeezing round.
328 if (measuredWidth > targetWidth) {
329 measuredWidth = originalMeasuredWidth;
330 maxChildHeight = originalMaxChildHeight;
331 buttonPaddingHorizontal = originalButtonPaddingHorizontal;
332
333 // Mark all buttons from the last squeezing round as "failed to squeeze", so
334 // that they're re-measured without squeezing later.
335 markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_FAILED, i);
336
337 // The current button doesn't fit, so there's no point in measuring further
338 // buttons.
339 break;
340 }
341
342 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
343 // to prevent them from being un-squeezed in a subsequent squeezing round.
344 markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, i);
345 }
346
347 lp.show = true;
348 displayedChildCount++;
349 }
350
351 // We're done squeezing buttons, so we can clear the priority queue.
352 mCandidateButtonQueueForSqueezing.clear();
353
Milo Sredkova5bacea2018-04-12 12:52:43 +0100354 // Finally, we need to re-measure some buttons.
355 remeasureButtonsIfNecessary(buttonPaddingHorizontal, maxChildHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000356
357 setMeasuredDimension(
358 resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec),
359 resolveSize(Math.max(getSuggestedMinimumHeight(),
360 mPaddingTop + maxChildHeight + mPaddingBottom), heightMeasureSpec));
361 }
362
363 private void resetButtonsLayoutParams() {
364 final int childCount = getChildCount();
365 for (int i = 0; i < childCount; i++) {
366 final View child = getChildAt(i);
367 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
368 lp.show = false;
369 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
370 }
371 }
372
373 private int squeezeButton(Button button, int heightMeasureSpec) {
374 final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
375 if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
376 return SQUEEZE_FAILED;
377 }
378 return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
379 }
380
381 private int estimateOptimalSqueezedButtonTextWidth(Button button) {
382 // Find a line-break point in the middle of the smart reply button text.
383 final String rawText = button.getText().toString();
384
385 // The button sometimes has a transformation affecting text layout (e.g. all caps).
386 final TransformationMethod transformation = button.getTransformationMethod();
387 final String text = transformation == null ?
388 rawText : transformation.getTransformation(rawText, button).toString();
389 final int length = text.length();
390 mBreakIterator.setText(text);
391
392 if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
393 if (mBreakIterator.next() == BreakIterator.DONE) {
394 // Can't find a single possible line break in either direction.
395 return SQUEEZE_FAILED;
396 }
397 }
398
399 final TextPaint paint = button.getPaint();
400 final int initialPosition = mBreakIterator.current();
401 final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
402 final float initialRightTextWidth =
403 Layout.getDesiredWidth(text, initialPosition, length, paint);
404 float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
405
406 if (initialLeftTextWidth != initialRightTextWidth) {
407 // See if there's a better line-break point (leading to a more narrow button) in
408 // either left or right direction.
409 final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
410 final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
411 for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
412 final int newPosition =
413 moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
414 if (newPosition == BreakIterator.DONE) {
415 break;
416 }
417
418 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
419 final float newRightTextWidth =
420 Layout.getDesiredWidth(text, newPosition, length, paint);
421 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
422 if (newOptimalTextWidth < optimalTextWidth) {
423 optimalTextWidth = newOptimalTextWidth;
424 } else {
425 break;
426 }
427
428 boolean tooFar = moveLeft
429 ? newLeftTextWidth <= newRightTextWidth
430 : newLeftTextWidth >= newRightTextWidth;
431 if (tooFar) {
432 break;
433 }
434 }
435 }
436
437 return (int) Math.ceil(optimalTextWidth);
438 }
439
440 private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
441 int oldWidth = button.getMeasuredWidth();
442 if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
443 // Correct for the fact that the button was laid out with single-line horizontal
444 // padding.
445 oldWidth += mSingleToDoubleLineButtonWidthIncrease;
446 }
447
448 // Re-measure the squeezed smart reply button.
449 button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
450 mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
451 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
452 2 * mDoubleLineButtonPaddingHorizontal + textWidth, MeasureSpec.AT_MOST);
453 button.measure(widthMeasureSpec, heightMeasureSpec);
454
455 final int newWidth = button.getMeasuredWidth();
456
457 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
458 if (button.getLineCount() > 2 || newWidth >= oldWidth) {
459 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
460 return SQUEEZE_FAILED;
461 } else {
462 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
463 return oldWidth - newWidth;
464 }
465 }
466
Milo Sredkova5bacea2018-04-12 12:52:43 +0100467 private void remeasureButtonsIfNecessary(
Petr Cermak102431d2018-01-29 10:36:07 +0000468 int buttonPaddingHorizontal, int maxChildHeight) {
Petr Cermak102431d2018-01-29 10:36:07 +0000469 final int maxChildHeightMeasure =
470 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
471
472 final int childCount = getChildCount();
473 for (int i = 0; i < childCount; i++) {
474 final View child = getChildAt(i);
475 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
476 if (!lp.show) {
477 continue;
478 }
479
Petr Cermak102431d2018-01-29 10:36:07 +0000480 boolean requiresNewMeasure = false;
481 int newWidth = child.getMeasuredWidth();
482
483 // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
484 // in more than two lines or because it was unnecessary).
485 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
486 requiresNewMeasure = true;
487 newWidth = Integer.MAX_VALUE;
488 }
489
490 // Re-measure reason 2: The button's horizontal padding is incorrect (because it was
491 // measured with the wrong number of lines).
492 if (child.getPaddingLeft() != buttonPaddingHorizontal) {
493 requiresNewMeasure = true;
Kenny Guy48ee6d62018-05-09 16:51:26 +0100494 if (newWidth != Integer.MAX_VALUE) {
495 if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) {
496 // Change padding (2->1 line).
497 newWidth -= mSingleToDoubleLineButtonWidthIncrease;
498 } else {
499 // Change padding (1->2 lines).
500 newWidth += mSingleToDoubleLineButtonWidthIncrease;
501 }
Petr Cermak102431d2018-01-29 10:36:07 +0000502 }
503 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
504 buttonPaddingHorizontal, child.getPaddingBottom());
505 }
506
507 // Re-measure reason 3: The button's height is less than the max height of all buttons
508 // (all should have the same height).
509 if (child.getMeasuredHeight() != maxChildHeight) {
510 requiresNewMeasure = true;
511 }
512
513 if (requiresNewMeasure) {
514 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
515 maxChildHeightMeasure);
516 }
517 }
518 }
519
520 private void markButtonsWithPendingSqueezeStatusAs(int squeezeStatus, int maxChildIndex) {
521 for (int i = 0; i <= maxChildIndex; i++) {
522 final View child = getChildAt(i);
523 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
524 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
525 lp.squeezeStatus = squeezeStatus;
526 }
527 }
528 }
529
530 @Override
531 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
532 final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
533
534 final int width = right - left;
535 int position = isRtl ? width - mPaddingRight : mPaddingLeft;
536
537 final int childCount = getChildCount();
538 for (int i = 0; i < childCount; i++) {
539 final View child = getChildAt(i);
540 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
541 if (!lp.show) {
542 continue;
543 }
544
545 final int childWidth = child.getMeasuredWidth();
546 final int childHeight = child.getMeasuredHeight();
547 final int childLeft = isRtl ? position - childWidth : position;
548 child.layout(childLeft, 0, childLeft + childWidth, childHeight);
549
550 final int childWidthWithSpacing = childWidth + mSpacing;
551 if (isRtl) {
552 position -= childWidthWithSpacing;
553 } else {
554 position += childWidthWithSpacing;
555 }
556 }
557 }
558
559 @Override
560 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
561 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
562 return lp.show && super.drawChild(canvas, child, drawingTime);
563 }
564
Kenny Guy14d035c2018-05-02 19:10:36 +0100565 public void setBackgroundTintColor(int backgroundColor) {
566 if (backgroundColor == mCurrentBackgroundColor) {
567 // Same color ignoring.
568 return;
569 }
570 mCurrentBackgroundColor = backgroundColor;
571
Lucas Dupina291d192018-06-07 13:59:42 -0700572 final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100573
Lucas Dupina291d192018-06-07 13:59:42 -0700574 int textColor = ContrastColorUtil.ensureTextContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100575 dark ? mDefaultTextColorDarkBg : mDefaultTextColor,
576 backgroundColor | 0xff000000, dark);
Lucas Dupina291d192018-06-07 13:59:42 -0700577 int strokeColor = ContrastColorUtil.ensureContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100578 mDefaultStrokeColor, backgroundColor | 0xff000000, dark, mMinStrokeContrast);
579 int rippleColor = dark ? mRippleColorDarkBg : mRippleColor;
580
581 int childCount = getChildCount();
582 for (int i = 0; i < childCount; i++) {
583 final Button child = (Button) getChildAt(i);
584 setColors(child, backgroundColor, strokeColor, textColor, rippleColor);
585 }
586 }
587
588 private void setColors(Button button, int backgroundColor, int strokeColor, int textColor,
589 int rippleColor) {
590 Drawable drawable = button.getBackground();
591 if (drawable instanceof RippleDrawable) {
592 // Mutate in case other notifications are using this drawable.
593 drawable = drawable.mutate();
594 RippleDrawable ripple = (RippleDrawable) drawable;
595 ripple.setColor(ColorStateList.valueOf(rippleColor));
596 Drawable inset = ripple.getDrawable(0);
597 if (inset instanceof InsetDrawable) {
598 Drawable background = ((InsetDrawable) inset).getDrawable();
599 if (background instanceof GradientDrawable) {
600 GradientDrawable gradientDrawable = (GradientDrawable) background;
601 gradientDrawable.setColor(backgroundColor);
602 gradientDrawable.setStroke(mStrokeWidth, strokeColor);
603 }
604 }
605 button.setBackground(drawable);
606 }
607 button.setTextColor(textColor);
608 }
609
Petr Cermak102431d2018-01-29 10:36:07 +0000610 @VisibleForTesting
611 static class LayoutParams extends ViewGroup.LayoutParams {
612
613 /** Button is not squeezed. */
614 private static final int SQUEEZE_STATUS_NONE = 0;
615
616 /**
617 * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
618 * turns out to have been unnecessary (because there's still not enough space to add another
619 * button).
620 */
621 private static final int SQUEEZE_STATUS_PENDING = 1;
622
623 /** Button was successfully squeezed and it won't be un-squeezed. */
624 private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
625
626 /**
627 * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
628 * text or it didn't reduce the button's width at all. The button will have to be
629 * re-measured to use only one line of text.
630 */
631 private static final int SQUEEZE_STATUS_FAILED = 3;
632
633 private boolean show = false;
634 private int squeezeStatus = SQUEEZE_STATUS_NONE;
635
636 private LayoutParams(Context c, AttributeSet attrs) {
637 super(c, attrs);
638 }
639
640 private LayoutParams(int width, int height) {
641 super(width, height);
642 }
643
644 @VisibleForTesting
645 boolean isShown() {
646 return show;
647 }
648 }
Petr Cermaked7429c2017-12-18 19:38:04 +0000649}