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