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