• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
addWordToDictionary(String word)74         public boolean addWordToDictionary(String word);
pickSuggestionManually(int index, CharSequence word, int x, int y)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 
UiHandler(SuggestionsView outerInstance)109         public UiHandler(SuggestionsView outerInstance) {
110             super(outerInstance);
111         }
112 
113         @Override
dispatchMessage(Message msg)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 
cancelHidePreview()123         public void cancelHidePreview() {
124             removeMessages(MSG_HIDE_PREVIEW);
125         }
126 
cancelAllMessages()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 
SuggestionsViewParams(Context context, AttributeSet attrs, int defStyle, ArrayList<TextView> words, ArrayList<View> dividers, ArrayList<TextView> infos)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 
getMaxMoreSuggestionsRow()242         public int getMaxMoreSuggestionsRow() {
243             return mMaxMoreSuggestionsRow;
244         }
245 
getMoreSuggestionsHeight()246         private int getMoreSuggestionsHeight() {
247             return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
248         }
249 
setMoreSuggestionsHeight(int remainingHeight)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 
getMoreSuggestionsHint(Resources res, float textSize, int color)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.
getPercent(TypedArray a, int index, int defValue)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.
getRatio(TypedArray a, int index)285         private static float getRatio(TypedArray a, int index) {
286             return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
287         }
288 
getStyledSuggestionWord(SuggestedWords suggestedWords, int pos)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 
getWordPosition(int index, SuggestedWords suggestedWords)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 
getSuggestionTextColor(int index, SuggestedWords suggestedWords, int pos)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 
applyAlpha(final int color, final float alpha)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 
addDivider(final ViewGroup stripView, final View divider)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 
layout(SuggestedWords suggestedWords, ViewGroup stripView, ViewGroup placer, int stripWidth)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 
getSuggestionWidth(int index, int maxWidth)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 
getSuggestionWeight(int index)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 
setupTexts(SuggestedWords suggestedWords, int countInStrip)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 
layoutPunctuationSuggestions(SuggestedWords suggestedWords, ViewGroup stripView)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 
layoutAddToDictionaryHint(CharSequence word, ViewGroup stripView, int stripWidth, CharSequence hintText, OnClickListener listener)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 
getAddToDictionaryWord()517         public CharSequence getAddToDictionaryWord() {
518             return (CharSequence)mWordToSaveView.getTag();
519         }
520 
isAddToDictionaryShowing(View v)521         public boolean isAddToDictionaryShowing(View v) {
522             return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
523         }
524 
setLayoutWeight(View v, float weight, int height)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 
getTextScaleX(CharSequence text, int maxWidth, TextPaint paint)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 
getEllipsizedText(CharSequence text, int maxWidth, TextPaint paint)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 
getTextWidth(CharSequence text, TextPaint paint)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 
getTextTypeface(CharSequence text)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      */
SuggestionsView(Context context, AttributeSet attrs)603     public SuggestionsView(Context context, AttributeSet attrs) {
604         this(context, attrs, R.attr.suggestionsViewStyle);
605     }
606 
SuggestionsView(Context context, AttributeSet attrs, int defStyle)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      */
setListener(Listener listener, View inputView)667     public void setListener(Listener listener, View inputView) {
668         mListener = listener;
669         mKeyboardView = (KeyboardView)inputView.findViewById(R.id.keyboard_view);
670     }
671 
setSuggestions(SuggestedWords suggestedWords)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 
setMoreSuggestionsHeight(int remainingHeight)684     public int setMoreSuggestionsHeight(int remainingHeight) {
685         return mParams.setMoreSuggestionsHeight(remainingHeight);
686     }
687 
isShowingAddToDictionaryHint()688     public boolean isShowingAddToDictionaryHint() {
689         return mSuggestionsStrip.getChildCount() > 0
690                 && mParams.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0));
691     }
692 
showAddToDictionaryHint(CharSequence word, CharSequence hintText)693     public void showAddToDictionaryHint(CharSequence word, CharSequence hintText) {
694         clear();
695         mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth(), hintText, this);
696     }
697 
dismissAddToDictionaryHint()698     public boolean dismissAddToDictionaryHint() {
699         if (isShowingAddToDictionaryHint()) {
700             clear();
701             return true;
702         }
703         return false;
704     }
705 
getSuggestions()706     public SuggestedWords getSuggestions() {
707         return mSuggestedWords;
708     }
709 
clear()710     public void clear() {
711         mSuggestionsStrip.removeAllViews();
712         removeAllViews();
713         addView(mSuggestionsStrip);
714         dismissMoreSuggestions();
715     }
716 
hidePreview()717     private void hidePreview() {
718         mPreviewPopup.dismiss();
719     }
720 
addToDictionary(CharSequence word)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 
dismissMoreSuggestions()752     private boolean dismissMoreSuggestions() {
753         if (mMoreSuggestionsWindow.isShowing()) {
754             mMoreSuggestionsWindow.dismiss();
755             return true;
756         }
757         return false;
758     }
759 
760     @Override
onLongClick(View view)761     public boolean onLongClick(View view) {
762         return showMoreSuggestions();
763     }
764 
showMoreSuggestions()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
dispatchTouchEvent(MotionEvent me)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
onClick(View view)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
onDetachedFromWindow()883     protected void onDetachedFromWindow() {
884         super.onDetachedFromWindow();
885         mHandler.cancelAllMessages();
886         hidePreview();
887         dismissMoreSuggestions();
888     }
889 }
890