1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
|
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.inputmethod.latin.suggestions;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.inputmethod.accessibility.AccessibilityUtils;
import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.PunctuationSuggestions;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
import com.android.inputmethod.latin.define.DebugFlags;
import com.android.inputmethod.latin.settings.Settings;
import com.android.inputmethod.latin.settings.SettingsValues;
import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
import com.android.inputmethod.latin.utils.ResourceUtils;
import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
import com.android.inputmethod.latin.utils.ViewLayoutUtils;
import java.util.ArrayList;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
final class SuggestionStripLayoutHelper {
private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f;
private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
private static final int PUNCTUATIONS_IN_STRIP = 5;
private static final float MIN_TEXT_XSCALE = 0.70f;
public final int mPadding;
public final int mDividerWidth;
public final int mSuggestionsStripHeight;
private final int mSuggestionsCountInStrip;
public final int mMoreSuggestionsRowHeight;
private int mMaxMoreSuggestionsRow;
public final float mMinMoreSuggestionsWidth;
public final int mMoreSuggestionsBottomGap;
private boolean mMoreSuggestionsAvailable;
// The index of these {@link ArrayList} is the position in the suggestion strip. The indices
// increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
// The position of the most important suggestion is in {@link #mCenterPositionInStrip}
private final ArrayList<TextView> mWordViews;
private final ArrayList<View> mDividerViews;
private final ArrayList<TextView> mDebugInfoViews;
private final int mColorValidTypedWord;
private final int mColorTypedWord;
private final int mColorAutoCorrect;
private final int mColorSuggested;
private final float mAlphaObsoleted;
private final float mCenterSuggestionWeight;
private final int mCenterPositionInStrip;
private final int mTypedWordPositionWhenAutocorrect;
private final Drawable mMoreSuggestionsHint;
private static final String MORE_SUGGESTIONS_HINT = "\u2026";
private static final String LEFTWARDS_ARROW = "\u2190";
private static final String RIGHTWARDS_ARROW = "\u2192";
private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
private final int mSuggestionStripOptions;
// These constants are the flag values of
// {@link R.styleable#SuggestionStripView_suggestionStripOptions} attribute.
private static final int AUTO_CORRECT_BOLD = 0x01;
private static final int AUTO_CORRECT_UNDERLINE = 0x02;
private static final int VALID_TYPED_WORD_BOLD = 0x04;
public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs,
final int defStyle, final ArrayList<TextView> wordViews,
final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) {
mWordViews = wordViews;
mDividerViews = dividerViews;
mDebugInfoViews = debugInfoViews;
final TextView wordView = wordViews.get(0);
final View dividerView = dividerViews.get(0);
mPadding = wordView.getCompoundPaddingLeft() + wordView.getCompoundPaddingRight();
dividerView.measure(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mDividerWidth = dividerView.getMeasuredWidth();
final Resources res = wordView.getResources();
mSuggestionsStripHeight = res.getDimensionPixelSize(
R.dimen.config_suggestions_strip_height);
final TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView);
mSuggestionStripOptions = a.getInt(
R.styleable.SuggestionStripView_suggestionStripOptions, 0);
mAlphaObsoleted = ResourceUtils.getFraction(a,
R.styleable.SuggestionStripView_alphaObsoleted, 1.0f);
mColorValidTypedWord = a.getColor(R.styleable.SuggestionStripView_colorValidTypedWord, 0);
mColorTypedWord = a.getColor(R.styleable.SuggestionStripView_colorTypedWord, 0);
mColorAutoCorrect = a.getColor(R.styleable.SuggestionStripView_colorAutoCorrect, 0);
mColorSuggested = a.getColor(R.styleable.SuggestionStripView_colorSuggested, 0);
mSuggestionsCountInStrip = a.getInt(
R.styleable.SuggestionStripView_suggestionsCountInStrip,
DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
mCenterSuggestionWeight = ResourceUtils.getFraction(a,
R.styleable.SuggestionStripView_centerSuggestionPercentile,
DEFAULT_CENTER_SUGGESTION_PERCENTILE);
mMaxMoreSuggestionsRow = a.getInt(
R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a,
R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f);
a.recycle();
mMoreSuggestionsHint = getMoreSuggestionsHint(res,
res.getDimension(R.dimen.config_more_suggestions_hint_text_size),
mColorAutoCorrect);
mCenterPositionInStrip = mSuggestionsCountInStrip / 2;
// Assuming there are at least three suggestions. Also, note that the suggestions are
// laid out according to script direction, so this is left of the center for LTR scripts
// and right of the center for RTL scripts.
mTypedWordPositionWhenAutocorrect = mCenterPositionInStrip - 1;
mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
R.dimen.config_more_suggestions_bottom_gap);
mMoreSuggestionsRowHeight = res.getDimensionPixelSize(
R.dimen.config_more_suggestions_row_height);
}
public int getMaxMoreSuggestionsRow() {
return mMaxMoreSuggestionsRow;
}
private int getMoreSuggestionsHeight() {
return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
}
public void setMoreSuggestionsHeight(final int remainingHeight) {
final int currentHeight = getMoreSuggestionsHeight();
if (currentHeight <= remainingHeight) {
return;
}
mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap)
/ mMoreSuggestionsRowHeight;
}
private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize,
final int color) {
final Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setTextAlign(Align.CENTER);
paint.setTextSize(textSize);
paint.setColor(color);
final Rect bounds = new Rect();
paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds);
final int width = Math.round(bounds.width() + 0.5f);
final int height = Math.round(bounds.height() + 0.5f);
final Bitmap buffer = Bitmap.createBitmap(width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(buffer);
canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint);
return new BitmapDrawable(res, buffer);
}
private CharSequence getStyledSuggestedWord(final SuggestedWords suggestedWords,
final int indexInSuggestedWords) {
if (indexInSuggestedWords >= suggestedWords.size()) {
return null;
}
final String word = suggestedWords.getLabel(indexInSuggestedWords);
// TODO: don't use the index to decide whether this is the auto-correction/typed word, as
// this is brittle
final boolean isAutoCorrection = suggestedWords.mWillAutoCorrect
&& indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION;
final boolean isTypedWordValid = suggestedWords.mTypedWordValid
&& indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD;
if (!isAutoCorrection && !isTypedWordValid) {
return word;
}
final Spannable spannedWord = new SpannableString(word);
final int options = mSuggestionStripOptions;
if ((isAutoCorrection && (options & AUTO_CORRECT_BOLD) != 0)
|| (isTypedWordValid && (options & VALID_TYPED_WORD_BOLD) != 0)) {
addStyleSpan(spannedWord, BOLD_SPAN);
}
if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) {
addStyleSpan(spannedWord, UNDERLINE_SPAN);
}
return spannedWord;
}
/**
* Convert an index of {@link SuggestedWords} to position in the suggestion strip.
* @param indexInSuggestedWords the index of {@link SuggestedWords}.
* @param suggestedWords the suggested words list
* @return Non-negative integer of the position in the suggestion strip.
* Negative integer if the word of the index shouldn't be shown on the suggestion strip.
*/
private int getPositionInSuggestionStrip(final int indexInSuggestedWords,
final SuggestedWords suggestedWords) {
final SettingsValues settingsValues = Settings.getInstance().getCurrent();
final boolean shouldOmitTypedWord = shouldOmitTypedWord(suggestedWords.mInputStyle,
settingsValues.mGestureFloatingPreviewTextEnabled,
settingsValues.mShouldShowLxxSuggestionUi);
return getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords.mWillAutoCorrect,
settingsValues.mShouldShowLxxSuggestionUi && shouldOmitTypedWord,
mCenterPositionInStrip, mTypedWordPositionWhenAutocorrect);
}
@UsedForTesting
static boolean shouldOmitTypedWord(final int inputStyle,
final boolean gestureFloatingPreviewTextEnabled,
final boolean shouldShowUiToAcceptTypedWord) {
final boolean omitTypedWord = (inputStyle == SuggestedWords.INPUT_STYLE_TYPING)
|| (inputStyle == SuggestedWords.INPUT_STYLE_TAIL_BATCH)
|| (inputStyle == SuggestedWords.INPUT_STYLE_UPDATE_BATCH
&& gestureFloatingPreviewTextEnabled);
return shouldShowUiToAcceptTypedWord && omitTypedWord;
}
@UsedForTesting
static int getPositionInSuggestionStrip(final int indexInSuggestedWords,
final boolean willAutoCorrect, final boolean omitTypedWord,
final int centerPositionInStrip, final int typedWordPositionWhenAutoCorrect) {
if (omitTypedWord) {
if (indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD) {
// Ignore.
return -1;
}
if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION) {
// Center in the suggestion strip.
return centerPositionInStrip;
}
// If neither of those, the order in the suggestion strip is left of the center first
// then right of the center, to both edges of the suggestion strip.
// For example, center-1, center+1, center-2, center+2, and so on.
final int n = indexInSuggestedWords;
final int offsetFromCenter = (n % 2) == 0 ? -(n / 2) : (n / 2);
final int positionInSuggestionStrip = centerPositionInStrip + offsetFromCenter;
return positionInSuggestionStrip;
}
final int indexToDisplayMostImportantSuggestion;
final int indexToDisplaySecondMostImportantSuggestion;
if (willAutoCorrect) {
indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION;
indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD;
} else {
indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD;
indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION;
}
if (indexInSuggestedWords == indexToDisplayMostImportantSuggestion) {
// Center in the suggestion strip.
return centerPositionInStrip;
}
if (indexInSuggestedWords == indexToDisplaySecondMostImportantSuggestion) {
// Center-1.
return typedWordPositionWhenAutoCorrect;
}
// If neither of those, the order in the suggestion strip is right of the center first
// then left of the center, to both edges of the suggestion strip.
// For example, Center+1, center-2, center+2, center-3, and so on.
final int n = indexInSuggestedWords + 1;
final int offsetFromCenter = (n % 2) == 0 ? -(n / 2) : (n / 2);
final int positionInSuggestionStrip = centerPositionInStrip + offsetFromCenter;
return positionInSuggestionStrip;
}
private int getSuggestionTextColor(final SuggestedWords suggestedWords,
final int indexInSuggestedWords) {
// Use identity for strings, not #equals : it's the typed word if it's the same object
final boolean isTypedWord = suggestedWords.getInfo(indexInSuggestedWords).isKindOf(
SuggestedWordInfo.KIND_TYPED);
final int color;
if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION
&& suggestedWords.mWillAutoCorrect) {
color = mColorAutoCorrect;
} else if (isTypedWord && suggestedWords.mTypedWordValid) {
color = mColorValidTypedWord;
} else if (isTypedWord) {
color = mColorTypedWord;
} else {
color = mColorSuggested;
}
if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) {
return applyAlpha(color, mAlphaObsoleted);
}
return color;
}
private static int applyAlpha(final int color, final float alpha) {
final int newAlpha = (int)(Color.alpha(color) * alpha);
return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
}
private static void addDivider(final ViewGroup stripView, final View dividerView) {
stripView.addView(dividerView);
final LinearLayout.LayoutParams params =
(LinearLayout.LayoutParams)dividerView.getLayoutParams();
params.gravity = Gravity.CENTER;
}
/**
* Layout suggestions to the suggestions strip. And returns the start index of more
* suggestions.
*
* @param suggestedWords suggestions to be shown in the suggestions strip.
* @param stripView the suggestions strip view.
* @param placerView the view where the debug info will be placed.
* @return the start index of more suggestions.
*/
public int layoutAndReturnStartIndexOfMoreSuggestions(final SuggestedWords suggestedWords,
final ViewGroup stripView, final ViewGroup placerView) {
if (suggestedWords.isPunctuationSuggestions()) {
return layoutPunctuationsAndReturnStartIndexOfMoreSuggestions(
(PunctuationSuggestions)suggestedWords, stripView);
}
final int wordCountToShow = suggestedWords.getWordCountToShow(
Settings.getInstance().getCurrent().mShouldShowLxxSuggestionUi);
final int startIndexOfMoreSuggestions = setupWordViewsAndReturnStartIndexOfMoreSuggestions(
suggestedWords, mSuggestionsCountInStrip);
final TextView centerWordView = mWordViews.get(mCenterPositionInStrip);
final int stripWidth = stripView.getWidth();
final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth);
if (wordCountToShow == 1 || getTextScaleX(centerWordView.getText(), centerWidth,
centerWordView.getPaint()) < MIN_TEXT_XSCALE) {
// Layout only the most relevant suggested word at the center of the suggestion strip
// by consolidating all slots in the strip.
final int countInStrip = 1;
mMoreSuggestionsAvailable = (wordCountToShow > countInStrip);
layoutWord(mCenterPositionInStrip, stripWidth - mPadding);
stripView.addView(centerWordView);
setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT);
if (SuggestionStripView.DBG) {
layoutDebugInfo(mCenterPositionInStrip, placerView, stripWidth);
}
final Integer lastIndex = (Integer)centerWordView.getTag();
return (lastIndex == null ? 0 : lastIndex) + 1;
}
final int countInStrip = mSuggestionsCountInStrip;
mMoreSuggestionsAvailable = (wordCountToShow > countInStrip);
int x = 0;
for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) {
if (positionInStrip != 0) {
final View divider = mDividerViews.get(positionInStrip);
// Add divider if this isn't the left most suggestion in suggestions strip.
addDivider(stripView, divider);
x += divider.getMeasuredWidth();
}
final int width = getSuggestionWidth(positionInStrip, stripWidth);
final TextView wordView = layoutWord(positionInStrip, width);
stripView.addView(wordView);
setLayoutWeight(wordView, getSuggestionWeight(positionInStrip),
ViewGroup.LayoutParams.MATCH_PARENT);
x += wordView.getMeasuredWidth();
if (SuggestionStripView.DBG) {
layoutDebugInfo(positionInStrip, placerView, x);
}
}
return startIndexOfMoreSuggestions;
}
/**
* Format appropriately the suggested word in {@link #mWordViews} specified by
* <code>positionInStrip</code>. When the suggested word doesn't exist, the corresponding
* {@link TextView} will be disabled and never respond to user interaction. The suggested word
* may be shrunk or ellipsized to fit in the specified width.
*
* The <code>positionInStrip</code> argument is the index in the suggestion strip. The indices
* increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
* The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This
* usually doesn't match the index in <code>suggedtedWords</code> -- see
* {@link #getPositionInSuggestionStrip(int,SuggestedWords)}.
*
* @param positionInStrip the position in the suggestion strip.
* @param width the maximum width for layout in pixels.
* @return the {@link TextView} containing the suggested word appropriately formatted.
*/
private TextView layoutWord(final int positionInStrip, final int width) {
final TextView wordView = mWordViews.get(positionInStrip);
final CharSequence word = wordView.getText();
if (positionInStrip == mCenterPositionInStrip && mMoreSuggestionsAvailable) {
// TODO: This "more suggestions hint" should have a nicely designed icon.
wordView.setCompoundDrawablesWithIntrinsicBounds(
null, null, null, mMoreSuggestionsHint);
// HACK: Align with other TextViews that have no compound drawables.
wordView.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
} else {
wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
// {@link StyleSpan} in a content description may cause an issue of TTS/TalkBack.
// Use a simple {@link String} to avoid the issue.
wordView.setContentDescription(TextUtils.isEmpty(word) ? null : word.toString());
final CharSequence text = getEllipsizedTextWithSettingScaleX(
word, width, wordView.getPaint());
final float scaleX = wordView.getTextScaleX();
wordView.setText(text); // TextView.setText() resets text scale x to 1.0.
wordView.setTextScaleX(scaleX);
// A <code>wordView</code> should be disabled when <code>word</code> is empty in order to
// make it unclickable.
// With accessibility touch exploration on, <code>wordView</code> should be enabled even
// when it is empty to avoid announcing as "disabled".
wordView.setEnabled(!TextUtils.isEmpty(word)
|| AccessibilityUtils.getInstance().isTouchExplorationEnabled());
return wordView;
}
private void layoutDebugInfo(final int positionInStrip, final ViewGroup placerView,
final int x) {
final TextView debugInfoView = mDebugInfoViews.get(positionInStrip);
final CharSequence debugInfo = debugInfoView.getText();
if (debugInfo == null) {
return;
}
placerView.addView(debugInfoView);
debugInfoView.measure(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
final int infoWidth = debugInfoView.getMeasuredWidth();
final int y = debugInfoView.getMeasuredHeight();
ViewLayoutUtils.placeViewAt(
debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight());
}
private int getSuggestionWidth(final int positionInStrip, final int maxWidth) {
final int paddings = mPadding * mSuggestionsCountInStrip;
final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
final int availableWidth = maxWidth - paddings - dividers;
return (int)(availableWidth * getSuggestionWeight(positionInStrip));
}
private float getSuggestionWeight(final int positionInStrip) {
if (positionInStrip == mCenterPositionInStrip) {
return mCenterSuggestionWeight;
}
// TODO: Revisit this for cases of 5 or more suggestions
return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1);
}
private int setupWordViewsAndReturnStartIndexOfMoreSuggestions(
final SuggestedWords suggestedWords, final int maxSuggestionInStrip) {
// Clear all suggestions first
for (int positionInStrip = 0; positionInStrip < maxSuggestionInStrip; ++positionInStrip) {
final TextView wordView = mWordViews.get(positionInStrip);
wordView.setText(null);
wordView.setTag(null);
// Make this inactive for touches in {@link #layoutWord(int,int)}.
if (SuggestionStripView.DBG) {
mDebugInfoViews.get(positionInStrip).setText(null);
}
}
int count = 0;
int indexInSuggestedWords;
for (indexInSuggestedWords = 0; indexInSuggestedWords < suggestedWords.size()
&& count < maxSuggestionInStrip; indexInSuggestedWords++) {
final int positionInStrip =
getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords);
if (positionInStrip < 0) {
continue;
}
final TextView wordView = mWordViews.get(positionInStrip);
// {@link TextView#getTag()} is used to get the index in suggestedWords at
// {@link SuggestionStripView#onClick(View)}.
wordView.setTag(indexInSuggestedWords);
wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords));
wordView.setTextColor(getSuggestionTextColor(suggestedWords, indexInSuggestedWords));
if (SuggestionStripView.DBG) {
mDebugInfoViews.get(positionInStrip).setText(
suggestedWords.getDebugString(indexInSuggestedWords));
}
count++;
}
return indexInSuggestedWords;
}
private int layoutPunctuationsAndReturnStartIndexOfMoreSuggestions(
final PunctuationSuggestions punctuationSuggestions, final ViewGroup stripView) {
final int countInStrip = Math.min(punctuationSuggestions.size(), PUNCTUATIONS_IN_STRIP);
for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) {
if (positionInStrip != 0) {
// Add divider if this isn't the left most suggestion in suggestions strip.
addDivider(stripView, mDividerViews.get(positionInStrip));
}
final TextView wordView = mWordViews.get(positionInStrip);
final String punctuation = punctuationSuggestions.getLabel(positionInStrip);
// {@link TextView#getTag()} is used to get the index in suggestedWords at
// {@link SuggestionStripView#onClick(View)}.
wordView.setTag(positionInStrip);
wordView.setText(punctuation);
wordView.setContentDescription(punctuation);
wordView.setTextScaleX(1.0f);
wordView.setCompoundDrawables(null, null, null, null);
wordView.setTextColor(mColorAutoCorrect);
stripView.addView(wordView);
setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight);
}
mMoreSuggestionsAvailable = (punctuationSuggestions.size() > countInStrip);
return countInStrip;
}
public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip,
final boolean shouldShowWordToSave) {
final boolean showsHintWithWord = shouldShowWordToSave
|| !Settings.getInstance().getCurrent().mShouldShowLxxSuggestionUi;
final int stripWidth = addToDictionaryStrip.getWidth();
final int width = stripWidth - (showsHintWithWord ? mDividerWidth + mPadding * 2 : 0);
final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save);
wordView.setTextColor(mColorTypedWord);
final int wordWidth = (int)(width * mCenterSuggestionWeight);
final CharSequence wordToSave = getEllipsizedTextWithSettingScaleX(
word, wordWidth, wordView.getPaint());
final float wordScaleX = wordView.getTextScaleX();
wordView.setText(wordToSave);
wordView.setTextScaleX(wordScaleX);
setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
final int wordVisibility = showsHintWithWord ? View.VISIBLE : View.GONE;
wordView.setVisibility(wordVisibility);
addToDictionaryStrip.findViewById(R.id.word_to_save_divider).setVisibility(wordVisibility);
final Resources res = addToDictionaryStrip.getResources();
final CharSequence hintText;
final int hintWidth;
final float hintWeight;
final TextView hintView = (TextView)addToDictionaryStrip.findViewById(
R.id.hint_add_to_dictionary);
if (showsHintWithWord) {
final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip)
== ViewCompat.LAYOUT_DIRECTION_RTL);
final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW;
final boolean isRtlSystem = SubtypeLocaleUtils.isRtlLanguage(
res.getConfiguration().locale);
final CharSequence hint = res.getText(R.string.hint_add_to_dictionary);
hintText = (isRtlLanguage == isRtlSystem) ? (arrow + hint) : (hint + arrow);
hintWidth = width - wordWidth;
hintWeight = 1.0f - mCenterSuggestionWeight;
hintView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
} else {
hintText = res.getText(R.string.hint_add_to_dictionary_without_word);
hintWidth = width;
hintWeight = 1.0f;
hintView.setGravity(Gravity.CENTER);
}
hintView.setTextColor(mColorAutoCorrect);
final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
hintView.setText(hintText); // TextView.setText() resets text scale x to 1.0.
hintView.setTextScaleX(hintScaleX);
setLayoutWeight(hintView, hintWeight, ViewGroup.LayoutParams.MATCH_PARENT);
}
public void layoutImportantNotice(final View importantNoticeStrip,
final String importantNoticeTitle) {
final TextView titleView = (TextView)importantNoticeStrip.findViewById(
R.id.important_notice_title);
final int width = titleView.getWidth() - titleView.getPaddingLeft()
- titleView.getPaddingRight();
titleView.setTextColor(mColorAutoCorrect);
titleView.setText(importantNoticeTitle); // TextView.setText() resets text scale x to 1.0.
final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint());
titleView.setTextScaleX(titleScaleX);
}
static void setLayoutWeight(final View v, final float weight, final int height) {
final ViewGroup.LayoutParams lp = v.getLayoutParams();
if (lp instanceof LinearLayout.LayoutParams) {
final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
llp.weight = weight;
llp.width = 0;
llp.height = height;
}
}
private static float getTextScaleX(@Nullable final CharSequence text, final int maxWidth,
final TextPaint paint) {
paint.setTextScaleX(1.0f);
final int width = getTextWidth(text, paint);
if (width <= maxWidth || maxWidth <= 0) {
return 1.0f;
}
return maxWidth / (float) width;
}
@Nullable
private static CharSequence getEllipsizedTextWithSettingScaleX(
@Nullable final CharSequence text, final int maxWidth, @Nonnull final TextPaint paint) {
if (text == null) {
return null;
}
final float scaleX = getTextScaleX(text, maxWidth, paint);
if (scaleX >= MIN_TEXT_XSCALE) {
paint.setTextScaleX(scaleX);
return text;
}
// <code>text</code> must be ellipsized with minimum text scale x.
paint.setTextScaleX(MIN_TEXT_XSCALE);
final boolean hasBoldStyle = hasStyleSpan(text, BOLD_SPAN);
final boolean hasUnderlineStyle = hasStyleSpan(text, UNDERLINE_SPAN);
// TextUtils.ellipsize erases any span object existed after ellipsized point.
// We have to restore these spans afterward.
final CharSequence ellipsizedText = TextUtils.ellipsize(
text, paint, maxWidth, TextUtils.TruncateAt.MIDDLE);
if (!hasBoldStyle && !hasUnderlineStyle) {
return ellipsizedText;
}
final Spannable spannableText = (ellipsizedText instanceof Spannable)
? (Spannable)ellipsizedText : new SpannableString(ellipsizedText);
if (hasBoldStyle) {
addStyleSpan(spannableText, BOLD_SPAN);
}
if (hasUnderlineStyle) {
addStyleSpan(spannableText, UNDERLINE_SPAN);
}
return spannableText;
}
private static boolean hasStyleSpan(@Nullable final CharSequence text,
final CharacterStyle style) {
if (text instanceof Spanned) {
return ((Spanned)text).getSpanStart(style) >= 0;
}
return false;
}
private static void addStyleSpan(@Nonnull final Spannable text, final CharacterStyle style) {
text.removeSpan(style);
text.setSpan(style, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
private static int getTextWidth(@Nullable final CharSequence text, final TextPaint paint) {
if (TextUtils.isEmpty(text)) {
return 0;
}
final int length = text.length();
final float[] widths = new float[length];
final int count;
final Typeface savedTypeface = paint.getTypeface();
try {
paint.setTypeface(getTextTypeface(text));
count = paint.getTextWidths(text, 0, length, widths);
} finally {
paint.setTypeface(savedTypeface);
}
int width = 0;
for (int i = 0; i < count; i++) {
width += Math.round(widths[i] + 0.5f);
}
return width;
}
private static Typeface getTextTypeface(@Nullable final CharSequence text) {
return hasStyleSpan(text, BOLD_SPAN) ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
}
}
|