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.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 
    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 
    162     public int getMaxMoreSuggestionsRow() {
    163         return mMaxMoreSuggestionsRow;
    164     }
    165 
    166     private int getMoreSuggestionsHeight() {
    167         return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
    168     }
    169 
    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 
    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 
    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      */
    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
    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
    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 
    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 
    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 
    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      */
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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
    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 
    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 
    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 
    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 
    647     private static Typeface getTextTypeface(@Nullable final CharSequence text) {
    648         return hasStyleSpan(text, BOLD_SPAN) ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT;
    649     }
    650 }
    651