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