Home | History | Annotate | Download | only in suggestions
      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 
    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 
    165     public int getMaxMoreSuggestionsRow() {
    166         return mMaxMoreSuggestionsRow;
    167     }
    168 
    169     private int getMoreSuggestionsHeight() {
    170         return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
    171     }
    172 
    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 
    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 
    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      */
    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
    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
    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 
    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 
    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 
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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