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