• 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.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 {
addWordToUserDictionary(String word)76         public void addWordToUserDictionary(String word);
pickSuggestionManually(int index, CharSequence 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 
UiHandler(SuggestionStripView outerInstance)111         public UiHandler(SuggestionStripView outerInstance) {
112             super(outerInstance);
113         }
114 
115         @Override
dispatchMessage(Message msg)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 
cancelHidePreview()125         public void cancelHidePreview() {
126             removeMessages(MSG_HIDE_PREVIEW);
127         }
128 
cancelAllMessages()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 
SuggestionStripViewParams(Context context, AttributeSet attrs, int defStyle, ArrayList<TextView> words, ArrayList<View> dividers, ArrayList<TextView> infos)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 
getMaxMoreSuggestionsRow()245         public int getMaxMoreSuggestionsRow() {
246             return mMaxMoreSuggestionsRow;
247         }
248 
getMoreSuggestionsHeight()249         private int getMoreSuggestionsHeight() {
250             return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
251         }
252 
setMoreSuggestionsHeight(int remainingHeight)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 
getMoreSuggestionsHint(Resources res, float textSize, int color)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 
getStyledSuggestionWord(SuggestedWords suggestedWords, int pos)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 
getWordPosition(int index, SuggestedWords suggestedWords)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 
getSuggestionTextColor(int index, SuggestedWords suggestedWords, int pos)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 
applyAlpha(final int color, final float alpha)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 
addDivider(final ViewGroup stripView, final View divider)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 
layout(SuggestedWords suggestedWords, ViewGroup stripView, ViewGroup placer, int stripWidth)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 
getSuggestionWidth(int index, int maxWidth)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 
getSuggestionWeight(int index)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 
setupTexts(SuggestedWords suggestedWords, int countInStrip)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 
layoutPunctuationSuggestions(SuggestedWords suggestedWords, ViewGroup stripView)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 
layoutAddToDictionaryHint(CharSequence word, ViewGroup stripView, int stripWidth, CharSequence hintText, OnClickListener listener)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 
getAddToDictionaryWord()510         public CharSequence getAddToDictionaryWord() {
511             return (CharSequence)mWordToSaveView.getTag();
512         }
513 
isAddToDictionaryShowing(View v)514         public boolean isAddToDictionaryShowing(View v) {
515             return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
516         }
517 
setLayoutWeight(View v, float weight, int height)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 
getTextScaleX(CharSequence text, int maxWidth, TextPaint paint)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 
getEllipsizedText(CharSequence text, int maxWidth, TextPaint paint)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 
getTextWidth(CharSequence text, TextPaint paint)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 
getTextTypeface(CharSequence text)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      */
SuggestionStripView(Context context, AttributeSet attrs)596     public SuggestionStripView(Context context, AttributeSet attrs) {
597         this(context, attrs, R.attr.suggestionStripViewStyle);
598     }
599 
SuggestionStripView(Context context, AttributeSet attrs, int defStyle)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      */
setListener(Listener listener, View inputView)661     public void setListener(Listener listener, View inputView) {
662         mListener = listener;
663         mKeyboardView = (KeyboardView)inputView.findViewById(R.id.keyboard_view);
664     }
665 
setSuggestions(SuggestedWords suggestedWords)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 
setMoreSuggestionsHeight(int remainingHeight)678     public int setMoreSuggestionsHeight(int remainingHeight) {
679         return mParams.setMoreSuggestionsHeight(remainingHeight);
680     }
681 
isShowingAddToDictionaryHint()682     public boolean isShowingAddToDictionaryHint() {
683         return mSuggestionsStrip.getChildCount() > 0
684                 && mParams.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0));
685     }
686 
showAddToDictionaryHint(CharSequence word, CharSequence hintText)687     public void showAddToDictionaryHint(CharSequence word, CharSequence hintText) {
688         clear();
689         mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth(), hintText, this);
690     }
691 
dismissAddToDictionaryHint()692     public boolean dismissAddToDictionaryHint() {
693         if (isShowingAddToDictionaryHint()) {
694             clear();
695             return true;
696         }
697         return false;
698     }
699 
getSuggestions()700     public SuggestedWords getSuggestions() {
701         return mSuggestedWords;
702     }
703 
clear()704     public void clear() {
705         mSuggestionsStrip.removeAllViews();
706         removeAllViews();
707         addView(mSuggestionsStrip);
708         dismissMoreSuggestions();
709     }
710 
hidePreview()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 
dismissMoreSuggestions()740     private boolean dismissMoreSuggestions() {
741         if (mMoreSuggestionsWindow.isShowing()) {
742             mMoreSuggestionsWindow.dismiss();
743             return true;
744         }
745         return false;
746     }
747 
748     @Override
onLongClick(View view)749     public boolean onLongClick(View view) {
750         return showMoreSuggestions();
751     }
752 
showMoreSuggestions()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
dispatchTouchEvent(MotionEvent me)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
onClick(View view)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
onDetachedFromWindow()871     protected void onDetachedFromWindow() {
872         super.onDetachedFromWindow();
873         mHandler.cancelAllMessages();
874         hidePreview();
875         dismissMoreSuggestions();
876     }
877 }
878