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