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