Home | History | Annotate | Download | only in suggestions
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * 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, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * 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.ColorDrawable;
     31 import android.graphics.drawable.Drawable;
     32 import android.os.Message;
     33 import android.text.Spannable;
     34 import android.text.SpannableString;
     35 import android.text.Spanned;
     36 import android.text.TextPaint;
     37 import android.text.TextUtils;
     38 import android.text.style.CharacterStyle;
     39 import android.text.style.StyleSpan;
     40 import android.text.style.UnderlineSpan;
     41 import android.util.AttributeSet;
     42 import android.view.GestureDetector;
     43 import android.view.Gravity;
     44 import android.view.LayoutInflater;
     45 import android.view.MotionEvent;
     46 import android.view.View;
     47 import android.view.View.OnClickListener;
     48 import android.view.View.OnLongClickListener;
     49 import android.view.ViewGroup;
     50 import android.widget.LinearLayout;
     51 import android.widget.PopupWindow;
     52 import android.widget.RelativeLayout;
     53 import android.widget.TextView;
     54 
     55 import com.android.inputmethod.keyboard.KeyboardActionListener;
     56 import com.android.inputmethod.keyboard.KeyboardView;
     57 import com.android.inputmethod.keyboard.MoreKeysPanel;
     58 import com.android.inputmethod.keyboard.PointerTracker;
     59 import com.android.inputmethod.keyboard.ViewLayoutUtils;
     60 import com.android.inputmethod.latin.AutoCorrection;
     61 import com.android.inputmethod.latin.CollectionUtils;
     62 import com.android.inputmethod.latin.LatinImeLogger;
     63 import com.android.inputmethod.latin.R;
     64 import com.android.inputmethod.latin.ResourceUtils;
     65 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
     66 import com.android.inputmethod.latin.SuggestedWords;
     67 import com.android.inputmethod.latin.Utils;
     68 import com.android.inputmethod.latin.define.ProductionFlag;
     69 import com.android.inputmethod.research.ResearchLogger;
     70 
     71 import java.util.ArrayList;
     72 
     73 public final class SuggestionStripView extends RelativeLayout implements OnClickListener,
     74         OnLongClickListener {
     75     public interface Listener {
     76         public boolean addWordToUserDictionary(String word);
     77         public void pickSuggestionManually(int index, CharSequence word);
     78     }
     79 
     80     // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
     81     public static final int MAX_SUGGESTIONS = 18;
     82 
     83     static final boolean DBG = LatinImeLogger.sDBG;
     84 
     85     private final ViewGroup mSuggestionsStrip;
     86     private KeyboardView mKeyboardView;
     87 
     88     private final View mMoreSuggestionsContainer;
     89     private final MoreSuggestionsView mMoreSuggestionsView;
     90     private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
     91     private final PopupWindow mMoreSuggestionsWindow;
     92 
     93     private final ArrayList<TextView> mWords = CollectionUtils.newArrayList();
     94     private final ArrayList<TextView> mInfos = CollectionUtils.newArrayList();
     95     private final ArrayList<View> mDividers = CollectionUtils.newArrayList();
     96 
     97     private final PopupWindow mPreviewPopup;
     98     private final TextView mPreviewText;
     99 
    100     private Listener mListener;
    101     private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
    102 
    103     private final SuggestionStripViewParams mParams;
    104     private static final float MIN_TEXT_XSCALE = 0.70f;
    105 
    106     private final UiHandler mHandler = new UiHandler(this);
    107 
    108     private static final class UiHandler extends StaticInnerHandlerWrapper<SuggestionStripView> {
    109         private static final int MSG_HIDE_PREVIEW = 0;
    110 
    111         public UiHandler(SuggestionStripView outerInstance) {
    112             super(outerInstance);
    113         }
    114 
    115         @Override
    116         public void dispatchMessage(Message msg) {
    117             final SuggestionStripView suggestionStripView = getOuterInstance();
    118             switch (msg.what) {
    119             case MSG_HIDE_PREVIEW:
    120                 suggestionStripView.hidePreview();
    121                 break;
    122             }
    123         }
    124 
    125         public void cancelHidePreview() {
    126             removeMessages(MSG_HIDE_PREVIEW);
    127         }
    128 
    129         public void cancelAllMessages() {
    130             cancelHidePreview();
    131         }
    132     }
    133 
    134     private static final class SuggestionStripViewParams {
    135         private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
    136         private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f;
    137         private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
    138         private static final int PUNCTUATIONS_IN_STRIP = 5;
    139 
    140         public final int mPadding;
    141         public final int mDividerWidth;
    142         public final int mSuggestionsStripHeight;
    143         public final int mSuggestionsCountInStrip;
    144         public final int mMoreSuggestionsRowHeight;
    145         private int mMaxMoreSuggestionsRow;
    146         public final float mMinMoreSuggestionsWidth;
    147         public final int mMoreSuggestionsBottomGap;
    148 
    149         private final ArrayList<TextView> mWords;
    150         private final ArrayList<View> mDividers;
    151         private final ArrayList<TextView> mInfos;
    152 
    153         private final int mColorValidTypedWord;
    154         private final int mColorTypedWord;
    155         private final int mColorAutoCorrect;
    156         private final int mColorSuggested;
    157         private final float mAlphaObsoleted;
    158         private final float mCenterSuggestionWeight;
    159         private final int mCenterSuggestionIndex;
    160         private final Drawable mMoreSuggestionsHint;
    161         private static final String MORE_SUGGESTIONS_HINT = "\u2026";
    162         private static final String LEFTWARDS_ARROW = "\u2190";
    163 
    164         private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
    165         private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
    166         private static final int AUTO_CORRECT_BOLD = 0x01;
    167         private static final int AUTO_CORRECT_UNDERLINE = 0x02;
    168         private static final int VALID_TYPED_WORD_BOLD = 0x04;
    169 
    170         private final int mSuggestionStripOption;
    171 
    172         private final ArrayList<CharSequence> mTexts = CollectionUtils.newArrayList();
    173 
    174         public boolean mMoreSuggestionsAvailable;
    175 
    176         private final TextView mWordToSaveView;
    177         private final TextView mLeftwardsArrowView;
    178         private final TextView mHintToSaveView;
    179 
    180         public SuggestionStripViewParams(Context context, AttributeSet attrs, int defStyle,
    181                 ArrayList<TextView> words, ArrayList<View> dividers, ArrayList<TextView> infos) {
    182             mWords = words;
    183             mDividers = dividers;
    184             mInfos = infos;
    185 
    186             final TextView word = words.get(0);
    187             final View divider = dividers.get(0);
    188             mPadding = word.getCompoundPaddingLeft() + word.getCompoundPaddingRight();
    189             divider.measure(
    190                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    191             mDividerWidth = divider.getMeasuredWidth();
    192 
    193             final Resources res = word.getResources();
    194             mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
    195 
    196             final TypedArray a = context.obtainStyledAttributes(attrs,
    197                     R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
    198             mSuggestionStripOption = a.getInt(
    199                     R.styleable.SuggestionStripView_suggestionStripOption, 0);
    200             final float alphaValidTypedWord = ResourceUtils.getFraction(a,
    201                     R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f);
    202             final float alphaTypedWord = ResourceUtils.getFraction(a,
    203                     R.styleable.SuggestionStripView_alphaTypedWord, 1.0f);
    204             final float alphaAutoCorrect = ResourceUtils.getFraction(a,
    205                     R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f);
    206             final float alphaSuggested = ResourceUtils.getFraction(a,
    207                     R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
    208             mAlphaObsoleted = ResourceUtils.getFraction(a,
    209                     R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
    210             mColorValidTypedWord = applyAlpha(a.getColor(
    211                     R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
    212             mColorTypedWord = applyAlpha(a.getColor(
    213                     R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord);
    214             mColorAutoCorrect = applyAlpha(a.getColor(
    215                     R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect);
    216             mColorSuggested = applyAlpha(a.getColor(
    217                     R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested);
    218             mSuggestionsCountInStrip = a.getInt(
    219                     R.styleable.SuggestionStripView_suggestionsCountInStrip,
    220                     DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
    221             mCenterSuggestionWeight = ResourceUtils.getFraction(a,
    222                     R.styleable.SuggestionStripView_centerSuggestionPercentile,
    223                     DEFAULT_CENTER_SUGGESTION_PERCENTILE);
    224             mMaxMoreSuggestionsRow = a.getInt(
    225                     R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
    226                     DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
    227             mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a,
    228                     R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f);
    229             a.recycle();
    230 
    231             mMoreSuggestionsHint = getMoreSuggestionsHint(res,
    232                     res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect);
    233             mCenterSuggestionIndex = mSuggestionsCountInStrip / 2;
    234             mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
    235                     R.dimen.more_suggestions_bottom_gap);
    236             mMoreSuggestionsRowHeight = res.getDimensionPixelSize(
    237                     R.dimen.more_suggestions_row_height);
    238 
    239             final LayoutInflater inflater = LayoutInflater.from(context);
    240             mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
    241             mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
    242             mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
    243         }
    244 
    245         public int getMaxMoreSuggestionsRow() {
    246             return mMaxMoreSuggestionsRow;
    247         }
    248 
    249         private int getMoreSuggestionsHeight() {
    250             return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
    251         }
    252 
    253         public int setMoreSuggestionsHeight(int remainingHeight) {
    254             final int currentHeight = getMoreSuggestionsHeight();
    255             if (currentHeight <= remainingHeight) {
    256                 return currentHeight;
    257             }
    258 
    259             mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap)
    260                     / mMoreSuggestionsRowHeight;
    261             final int newHeight = getMoreSuggestionsHeight();
    262             return newHeight;
    263         }
    264 
    265         private static Drawable getMoreSuggestionsHint(Resources res, float textSize, int color) {
    266             final Paint paint = new Paint();
    267             paint.setAntiAlias(true);
    268             paint.setTextAlign(Align.CENTER);
    269             paint.setTextSize(textSize);
    270             paint.setColor(color);
    271             final Rect bounds = new Rect();
    272             paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds);
    273             final int width = Math.round(bounds.width() + 0.5f);
    274             final int height = Math.round(bounds.height() + 0.5f);
    275             final Bitmap buffer = Bitmap.createBitmap(
    276                     width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
    277             final Canvas canvas = new Canvas(buffer);
    278             canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint);
    279             return new BitmapDrawable(res, buffer);
    280         }
    281 
    282         private CharSequence getStyledSuggestionWord(SuggestedWords suggestedWords, int pos) {
    283             final CharSequence word = suggestedWords.getWord(pos);
    284             final boolean isAutoCorrect = pos == 1 && suggestedWords.willAutoCorrect();
    285             final boolean isTypedWordValid = pos == 0 && suggestedWords.mTypedWordValid;
    286             if (!isAutoCorrect && !isTypedWordValid)
    287                 return word;
    288 
    289             final int len = word.length();
    290             final Spannable spannedWord = new SpannableString(word);
    291             final int option = mSuggestionStripOption;
    292             if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0)
    293                     || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) {
    294                 spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    295             }
    296             if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) {
    297                 spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    298             }
    299             return spannedWord;
    300         }
    301 
    302         private int getWordPosition(int index, SuggestedWords suggestedWords) {
    303             // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
    304             // suggestions.
    305             final int centerPos = suggestedWords.willAutoCorrect() ? 1 : 0;
    306             if (index == mCenterSuggestionIndex) {
    307                 return centerPos;
    308             } else if (index == centerPos) {
    309                 return mCenterSuggestionIndex;
    310             } else {
    311                 return index;
    312             }
    313         }
    314 
    315         private int getSuggestionTextColor(int index, SuggestedWords suggestedWords, int pos) {
    316             // TODO: Need to revisit this logic with bigram suggestions
    317             final boolean isSuggested = (pos != 0);
    318 
    319             final int color;
    320             if (index == mCenterSuggestionIndex && suggestedWords.willAutoCorrect()) {
    321                 color = mColorAutoCorrect;
    322             } else if (index == mCenterSuggestionIndex && suggestedWords.mTypedWordValid) {
    323                 color = mColorValidTypedWord;
    324             } else if (isSuggested) {
    325                 color = mColorSuggested;
    326             } else {
    327                 color = mColorTypedWord;
    328             }
    329             if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
    330                 // If we auto-correct, then the autocorrection is in slot 0 and the typed word
    331                 // is in slot 1.
    332                 if (index == mCenterSuggestionIndex
    333                         && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
    334                                 suggestedWords.getWord(1).toString(), suggestedWords.getWord(0))) {
    335                     return 0xFFFF0000;
    336                 }
    337             }
    338 
    339             if (suggestedWords.mIsObsoleteSuggestions && isSuggested) {
    340                 return applyAlpha(color, mAlphaObsoleted);
    341             } else {
    342                 return color;
    343             }
    344         }
    345 
    346         private static int applyAlpha(final int color, final float alpha) {
    347             final int newAlpha = (int)(Color.alpha(color) * alpha);
    348             return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
    349         }
    350 
    351         private static void addDivider(final ViewGroup stripView, final View divider) {
    352             stripView.addView(divider);
    353             final LinearLayout.LayoutParams params =
    354                     (LinearLayout.LayoutParams)divider.getLayoutParams();
    355             params.gravity = Gravity.CENTER;
    356         }
    357 
    358         public void layout(SuggestedWords suggestedWords, ViewGroup stripView, ViewGroup placer,
    359                 int stripWidth) {
    360             if (suggestedWords.mIsPunctuationSuggestions) {
    361                 layoutPunctuationSuggestions(suggestedWords, stripView);
    362                 return;
    363             }
    364 
    365             final int countInStrip = mSuggestionsCountInStrip;
    366             setupTexts(suggestedWords, countInStrip);
    367             mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
    368             int x = 0;
    369             for (int index = 0; index < countInStrip; index++) {
    370                 final int pos = getWordPosition(index, suggestedWords);
    371 
    372                 if (index != 0) {
    373                     final View divider = mDividers.get(pos);
    374                     // Add divider if this isn't the left most suggestion in suggestions strip.
    375                     addDivider(stripView, divider);
    376                     x += divider.getMeasuredWidth();
    377                 }
    378 
    379                 final CharSequence styled = mTexts.get(pos);
    380                 final TextView word = mWords.get(pos);
    381                 if (index == mCenterSuggestionIndex && mMoreSuggestionsAvailable) {
    382                     // TODO: This "more suggestions hint" should have nicely designed icon.
    383                     word.setCompoundDrawablesWithIntrinsicBounds(
    384                             null, null, null, mMoreSuggestionsHint);
    385                     // HACK: To align with other TextView that has no compound drawables.
    386                     word.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
    387                 } else {
    388                     word.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
    389                 }
    390 
    391                 // Disable this suggestion if the suggestion is null or empty.
    392                 word.setEnabled(!TextUtils.isEmpty(styled));
    393                 word.setTextColor(getSuggestionTextColor(index, suggestedWords, pos));
    394                 final int width = getSuggestionWidth(index, stripWidth);
    395                 final CharSequence text = getEllipsizedText(styled, width, word.getPaint());
    396                 final float scaleX = word.getTextScaleX();
    397                 word.setText(text); // TextView.setText() resets text scale x to 1.0.
    398                 word.setTextScaleX(scaleX);
    399                 stripView.addView(word);
    400                 setLayoutWeight(
    401                         word, getSuggestionWeight(index), ViewGroup.LayoutParams.MATCH_PARENT);
    402                 x += word.getMeasuredWidth();
    403 
    404                 if (DBG && pos < suggestedWords.size()) {
    405                     final CharSequence debugInfo = Utils.getDebugInfo(suggestedWords, pos);
    406                     if (debugInfo != null) {
    407                         final TextView info = mInfos.get(pos);
    408                         info.setText(debugInfo);
    409                         placer.addView(info);
    410                         info.measure(ViewGroup.LayoutParams.WRAP_CONTENT,
    411                                 ViewGroup.LayoutParams.WRAP_CONTENT);
    412                         final int infoWidth = info.getMeasuredWidth();
    413                         final int y = info.getMeasuredHeight();
    414                         ViewLayoutUtils.placeViewAt(
    415                                 info, x - infoWidth, y, infoWidth, info.getMeasuredHeight());
    416                     }
    417                 }
    418             }
    419         }
    420 
    421         private int getSuggestionWidth(int index, int maxWidth) {
    422             final int paddings = mPadding * mSuggestionsCountInStrip;
    423             final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
    424             final int availableWidth = maxWidth - paddings - dividers;
    425             return (int)(availableWidth * getSuggestionWeight(index));
    426         }
    427 
    428         private float getSuggestionWeight(int index) {
    429             if (index == mCenterSuggestionIndex) {
    430                 return mCenterSuggestionWeight;
    431             } else {
    432                 // TODO: Revisit this for cases of 5 or more suggestions
    433                 return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1);
    434             }
    435         }
    436 
    437         private void setupTexts(SuggestedWords suggestedWords, int countInStrip) {
    438             mTexts.clear();
    439             final int count = Math.min(suggestedWords.size(), countInStrip);
    440             for (int pos = 0; pos < count; pos++) {
    441                 final CharSequence styled = getStyledSuggestionWord(suggestedWords, pos);
    442                 mTexts.add(styled);
    443             }
    444             for (int pos = count; pos < countInStrip; pos++) {
    445                 // Make this inactive for touches in layout().
    446                 mTexts.add(null);
    447             }
    448         }
    449 
    450         private void layoutPunctuationSuggestions(SuggestedWords suggestedWords,
    451                 ViewGroup stripView) {
    452             final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP);
    453             for (int index = 0; index < countInStrip; index++) {
    454                 if (index != 0) {
    455                     // Add divider if this isn't the left most suggestion in suggestions strip.
    456                     addDivider(stripView, mDividers.get(index));
    457                 }
    458 
    459                 final TextView word = mWords.get(index);
    460                 word.setEnabled(true);
    461                 word.setTextColor(mColorAutoCorrect);
    462                 final CharSequence text = suggestedWords.getWord(index);
    463                 word.setText(text);
    464                 word.setTextScaleX(1.0f);
    465                 word.setCompoundDrawables(null, null, null, null);
    466                 stripView.addView(word);
    467                 setLayoutWeight(word, 1.0f, mSuggestionsStripHeight);
    468             }
    469             mMoreSuggestionsAvailable = false;
    470         }
    471 
    472         public void layoutAddToDictionaryHint(CharSequence word, ViewGroup stripView,
    473                 int stripWidth, CharSequence hintText, OnClickListener listener) {
    474             final int width = stripWidth - mDividerWidth - mPadding * 2;
    475 
    476             final TextView wordView = mWordToSaveView;
    477             wordView.setTextColor(mColorTypedWord);
    478             final int wordWidth = (int)(width * mCenterSuggestionWeight);
    479             final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
    480             final float wordScaleX = wordView.getTextScaleX();
    481             wordView.setTag(word);
    482             wordView.setText(text);
    483             wordView.setTextScaleX(wordScaleX);
    484             stripView.addView(wordView);
    485             setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
    486 
    487             stripView.addView(mDividers.get(0));
    488 
    489             final TextView leftArrowView = mLeftwardsArrowView;
    490             leftArrowView.setTextColor(mColorAutoCorrect);
    491             leftArrowView.setText(LEFTWARDS_ARROW);
    492             stripView.addView(leftArrowView);
    493 
    494             final TextView hintView = mHintToSaveView;
    495             hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
    496             hintView.setTextColor(mColorAutoCorrect);
    497             final int hintWidth = width - wordWidth - leftArrowView.getWidth();
    498             final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
    499             hintView.setText(hintText);
    500             hintView.setTextScaleX(hintScaleX);
    501             stripView.addView(hintView);
    502             setLayoutWeight(
    503                     hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
    504 
    505             wordView.setOnClickListener(listener);
    506             leftArrowView.setOnClickListener(listener);
    507             hintView.setOnClickListener(listener);
    508         }
    509 
    510         public CharSequence getAddToDictionaryWord() {
    511             return (CharSequence)mWordToSaveView.getTag();
    512         }
    513 
    514         public boolean isAddToDictionaryShowing(View v) {
    515             return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
    516         }
    517 
    518         private static void setLayoutWeight(View v, float weight, int height) {
    519             final ViewGroup.LayoutParams lp = v.getLayoutParams();
    520             if (lp instanceof LinearLayout.LayoutParams) {
    521                 final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
    522                 llp.weight = weight;
    523                 llp.width = 0;
    524                 llp.height = height;
    525             }
    526         }
    527 
    528         private static float getTextScaleX(CharSequence text, int maxWidth, TextPaint paint) {
    529             paint.setTextScaleX(1.0f);
    530             final int width = getTextWidth(text, paint);
    531             if (width <= maxWidth) {
    532                 return 1.0f;
    533             }
    534             return maxWidth / (float)width;
    535         }
    536 
    537         private static CharSequence getEllipsizedText(CharSequence text, int maxWidth,
    538                 TextPaint paint) {
    539             if (text == null) return null;
    540             paint.setTextScaleX(1.0f);
    541             final int width = getTextWidth(text, paint);
    542             if (width <= maxWidth) {
    543                 return text;
    544             }
    545             final float scaleX = maxWidth / (float)width;
    546             if (scaleX >= MIN_TEXT_XSCALE) {
    547                 paint.setTextScaleX(scaleX);
    548                 return text;
    549             }
    550 
    551             // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To
    552             // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
    553             final CharSequence ellipsized = TextUtils.ellipsize(
    554                     text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE);
    555             paint.setTextScaleX(MIN_TEXT_XSCALE);
    556             return ellipsized;
    557         }
    558 
    559         private static int getTextWidth(CharSequence text, TextPaint paint) {
    560             if (TextUtils.isEmpty(text)) return 0;
    561             final Typeface savedTypeface = paint.getTypeface();
    562             paint.setTypeface(getTextTypeface(text));
    563             final int len = text.length();
    564             final float[] widths = new float[len];
    565             final int count = paint.getTextWidths(text, 0, len, widths);
    566             int width = 0;
    567             for (int i = 0; i < count; i++) {
    568                 width += Math.round(widths[i] + 0.5f);
    569             }
    570             paint.setTypeface(savedTypeface);
    571             return width;
    572         }
    573 
    574         private static Typeface getTextTypeface(CharSequence text) {
    575             if (!(text instanceof SpannableString))
    576                 return Typeface.DEFAULT;
    577 
    578             final SpannableString ss = (SpannableString)text;
    579             final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class);
    580             if (styles.length == 0)
    581                 return Typeface.DEFAULT;
    582 
    583             switch (styles[0].getStyle()) {
    584             case Typeface.BOLD: return Typeface.DEFAULT_BOLD;
    585             // TODO: BOLD_ITALIC, ITALIC case?
    586             default: return Typeface.DEFAULT;
    587             }
    588         }
    589     }
    590 
    591     /**
    592      * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user.
    593      * @param context
    594      * @param attrs
    595      */
    596     public SuggestionStripView(Context context, AttributeSet attrs) {
    597         this(context, attrs, R.attr.suggestionStripViewStyle);
    598     }
    599 
    600     public SuggestionStripView(Context context, AttributeSet attrs, int defStyle) {
    601         super(context, attrs, defStyle);
    602 
    603         final LayoutInflater inflater = LayoutInflater.from(context);
    604         inflater.inflate(R.layout.suggestions_strip, this);
    605 
    606         mPreviewPopup = new PopupWindow(context);
    607         mPreviewText = (TextView) inflater.inflate(R.layout.suggestion_preview, null);
    608         mPreviewPopup.setWindowLayoutMode(
    609                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    610         mPreviewPopup.setContentView(mPreviewText);
    611         mPreviewPopup.setBackgroundDrawable(null);
    612 
    613         mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip);
    614         for (int pos = 0; pos < MAX_SUGGESTIONS; pos++) {
    615             final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null);
    616             word.setTag(pos);
    617             word.setOnClickListener(this);
    618             word.setOnLongClickListener(this);
    619             mWords.add(word);
    620             final View divider = inflater.inflate(R.layout.suggestion_divider, null);
    621             divider.setTag(pos);
    622             divider.setOnClickListener(this);
    623             mDividers.add(divider);
    624             mInfos.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
    625         }
    626 
    627         mParams = new SuggestionStripViewParams(
    628                 context, attrs, defStyle, mWords, mDividers, mInfos);
    629 
    630         mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
    631         mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
    632                 .findViewById(R.id.more_suggestions_view);
    633         mMoreSuggestionsBuilder = new MoreSuggestions.Builder(mMoreSuggestionsView);
    634 
    635         final PopupWindow moreWindow = new PopupWindow(context);
    636         moreWindow.setWindowLayoutMode(
    637                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    638         moreWindow.setBackgroundDrawable(new ColorDrawable(android.R.color.transparent));
    639         moreWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
    640         moreWindow.setFocusable(true);
    641         moreWindow.setOutsideTouchable(true);
    642         moreWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
    643             @Override
    644             public void onDismiss() {
    645                 mKeyboardView.dimEntireKeyboard(false);
    646             }
    647         });
    648         mMoreSuggestionsWindow = moreWindow;
    649 
    650         final Resources res = context.getResources();
    651         mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset(
    652                 R.dimen.more_suggestions_modal_tolerance);
    653         mMoreSuggestionsSlidingDetector = new GestureDetector(
    654                 context, mMoreSuggestionsSlidingListener);
    655     }
    656 
    657     /**
    658      * A connection back to the input method.
    659      * @param listener
    660      */
    661     public void setListener(Listener listener, View inputView) {
    662         mListener = listener;
    663         mKeyboardView = (KeyboardView)inputView.findViewById(R.id.keyboard_view);
    664     }
    665 
    666     public void setSuggestions(SuggestedWords suggestedWords) {
    667         if (suggestedWords == null)
    668             return;
    669 
    670         clear();
    671         mSuggestedWords = suggestedWords;
    672         mParams.layout(mSuggestedWords, mSuggestionsStrip, this, getWidth());
    673         if (ProductionFlag.IS_EXPERIMENTAL) {
    674             ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords);
    675         }
    676     }
    677 
    678     public int setMoreSuggestionsHeight(int remainingHeight) {
    679         return mParams.setMoreSuggestionsHeight(remainingHeight);
    680     }
    681 
    682     public boolean isShowingAddToDictionaryHint() {
    683         return mSuggestionsStrip.getChildCount() > 0
    684                 && mParams.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0));
    685     }
    686 
    687     public void showAddToDictionaryHint(CharSequence word, CharSequence hintText) {
    688         clear();
    689         mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth(), hintText, this);
    690     }
    691 
    692     public boolean dismissAddToDictionaryHint() {
    693         if (isShowingAddToDictionaryHint()) {
    694             clear();
    695             return true;
    696         }
    697         return false;
    698     }
    699 
    700     public SuggestedWords getSuggestions() {
    701         return mSuggestedWords;
    702     }
    703 
    704     public void clear() {
    705         mSuggestionsStrip.removeAllViews();
    706         removeAllViews();
    707         addView(mSuggestionsStrip);
    708         dismissMoreSuggestions();
    709     }
    710 
    711     private void hidePreview() {
    712         mPreviewPopup.dismiss();
    713     }
    714 
    715     private final KeyboardActionListener mMoreSuggestionsListener =
    716             new KeyboardActionListener.Adapter() {
    717         @Override
    718         public boolean onCustomRequest(int requestCode) {
    719             final int index = requestCode;
    720             final CharSequence word = mSuggestedWords.getWord(index);
    721             mListener.pickSuggestionManually(index, word);
    722             dismissMoreSuggestions();
    723             return true;
    724         }
    725 
    726         @Override
    727         public void onCancelInput() {
    728             dismissMoreSuggestions();
    729         }
    730     };
    731 
    732     private final MoreKeysPanel.Controller mMoreSuggestionsController =
    733             new MoreKeysPanel.Controller() {
    734         @Override
    735         public boolean dismissMoreKeysPanel() {
    736             return dismissMoreSuggestions();
    737         }
    738     };
    739 
    740     private boolean dismissMoreSuggestions() {
    741         if (mMoreSuggestionsWindow.isShowing()) {
    742             mMoreSuggestionsWindow.dismiss();
    743             return true;
    744         }
    745         return false;
    746     }
    747 
    748     @Override
    749     public boolean onLongClick(View view) {
    750         return showMoreSuggestions();
    751     }
    752 
    753     private boolean showMoreSuggestions() {
    754         final SuggestionStripViewParams params = mParams;
    755         if (params.mMoreSuggestionsAvailable) {
    756             final int stripWidth = getWidth();
    757             final View container = mMoreSuggestionsContainer;
    758             final int maxWidth = stripWidth - container.getPaddingLeft()
    759                     - container.getPaddingRight();
    760             final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder;
    761             builder.layout(mSuggestedWords, params.mSuggestionsCountInStrip, maxWidth,
    762                     (int)(maxWidth * params.mMinMoreSuggestionsWidth),
    763                     params.getMaxMoreSuggestionsRow());
    764             mMoreSuggestionsView.setKeyboard(builder.build());
    765             container.measure(
    766                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    767 
    768             final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
    769             final int pointX = stripWidth / 2;
    770             final int pointY = -params.mMoreSuggestionsBottomGap;
    771             moreKeysPanel.showMoreKeysPanel(
    772                     this, mMoreSuggestionsController, pointX, pointY,
    773                     mMoreSuggestionsWindow, mMoreSuggestionsListener);
    774             mMoreSuggestionsMode = MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING;
    775             mOriginX = mLastX;
    776             mOriginY = mLastY;
    777             mKeyboardView.dimEntireKeyboard(true);
    778             for (int i = 0; i < params.mSuggestionsCountInStrip; i++) {
    779                 mWords.get(i).setPressed(false);
    780             }
    781             return true;
    782         }
    783         return false;
    784     }
    785 
    786     // Working variables for onLongClick and dispatchTouchEvent.
    787     private int mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE;
    788     private static final int MORE_SUGGESTIONS_IN_MODAL_MODE = 0;
    789     private static final int MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING = 1;
    790     private static final int MORE_SUGGESTIONS_IN_SLIDING_MODE = 2;
    791     private int mLastX;
    792     private int mLastY;
    793     private int mOriginX;
    794     private int mOriginY;
    795     private final int mMoreSuggestionsModalTolerance;
    796     private final GestureDetector mMoreSuggestionsSlidingDetector;
    797     private final GestureDetector.OnGestureListener mMoreSuggestionsSlidingListener =
    798             new GestureDetector.SimpleOnGestureListener() {
    799         @Override
    800         public boolean onScroll(MotionEvent down, MotionEvent me, float deltaX, float deltaY) {
    801             final float dy = me.getY() - down.getY();
    802             if (deltaY > 0 && dy < 0) {
    803                 return showMoreSuggestions();
    804             }
    805             return false;
    806         }
    807     };
    808 
    809     @Override
    810     public boolean dispatchTouchEvent(MotionEvent me) {
    811         if (!mMoreSuggestionsWindow.isShowing()
    812                 || mMoreSuggestionsMode == MORE_SUGGESTIONS_IN_MODAL_MODE) {
    813             mLastX = (int)me.getX();
    814             mLastY = (int)me.getY();
    815             if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) {
    816                 return true;
    817             }
    818             return super.dispatchTouchEvent(me);
    819         }
    820 
    821         final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
    822         final int action = me.getAction();
    823         final long eventTime = me.getEventTime();
    824         final int index = me.getActionIndex();
    825         final int id = me.getPointerId(index);
    826         final PointerTracker tracker = PointerTracker.getPointerTracker(id, moreKeysPanel);
    827         final int x = (int)me.getX(index);
    828         final int y = (int)me.getY(index);
    829         final int translatedX = moreKeysPanel.translateX(x);
    830         final int translatedY = moreKeysPanel.translateY(y);
    831 
    832         if (mMoreSuggestionsMode == MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING) {
    833             if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance
    834                     || mOriginY - y >= mMoreSuggestionsModalTolerance) {
    835                 // Decided to be in the sliding input mode only when the touch point has been moved
    836                 // upward.
    837                 mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_SLIDING_MODE;
    838                 tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
    839             } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
    840                 // Decided to be in the modal input mode
    841                 mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE;
    842             }
    843             return true;
    844         }
    845 
    846         // MORE_SUGGESTIONS_IN_SLIDING_MODE
    847         tracker.processMotionEvent(action, translatedX, translatedY, eventTime, moreKeysPanel);
    848         return true;
    849     }
    850 
    851     @Override
    852     public void onClick(View view) {
    853         if (mParams.isAddToDictionaryShowing(view)) {
    854             mListener.addWordToUserDictionary(mParams.getAddToDictionaryWord().toString());
    855             clear();
    856             return;
    857         }
    858 
    859         final Object tag = view.getTag();
    860         if (!(tag instanceof Integer))
    861             return;
    862         final int index = (Integer) tag;
    863         if (index >= mSuggestedWords.size())
    864             return;
    865 
    866         final CharSequence word = mSuggestedWords.getWord(index);
    867         mListener.pickSuggestionManually(index, word);
    868     }
    869 
    870     @Override
    871     protected void onDetachedFromWindow() {
    872         super.onDetachedFromWindow();
    873         mHandler.cancelAllMessages();
    874         hidePreview();
    875         dismissMoreSuggestions();
    876     }
    877 }
    878