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