Skip to content
Snippets Groups Projects
SuggestionsView.java 35.4 KiB
Newer Older
 * Copyright (C) 2011 The Android Open Source Project
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.android.inputmethod.latin;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
satok's avatar
satok committed
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Message;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import com.android.inputmethod.compat.FrameLayoutCompatUtils;
import com.android.inputmethod.keyboard.KeyboardActionListener;
import com.android.inputmethod.keyboard.KeyboardView;
import com.android.inputmethod.keyboard.MoreKeysPanel;
import com.android.inputmethod.keyboard.PointerTracker;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;

import java.util.ArrayList;
satok's avatar
satok committed
import java.util.List;
public class SuggestionsView extends RelativeLayout implements OnClickListener,
        OnLongClickListener {
    public interface Listener {
        public boolean addWordToDictionary(String word);
        public void pickSuggestionManually(int index, CharSequence word);
    }

    // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
    public static final int MAX_SUGGESTIONS = 18;
    private static final boolean DBG = LatinImeLogger.sDBG;
    private final ViewGroup mSuggestionsStrip;
    private KeyboardView mKeyboardView;
    private final View mMoreSuggestionsContainer;
    private final MoreSuggestionsView mMoreSuggestionsView;
    private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
    private final PopupWindow mMoreSuggestionsWindow;

    private final ArrayList<TextView> mWords = new ArrayList<TextView>();
    private final ArrayList<TextView> mInfos = new ArrayList<TextView>();
    private final ArrayList<View> mDividers = new ArrayList<View>();
    private final PopupWindow mPreviewPopup;
    private final TextView mPreviewText;
    private Listener mListener;
    private SuggestedWords mSuggestions = SuggestedWords.EMPTY;
    private final SuggestionsViewParams mParams;
    private static final float MIN_TEXT_XSCALE = 0.70f;
    private final UiHandler mHandler = new UiHandler(this);
    private static class UiHandler extends StaticInnerHandlerWrapper<SuggestionsView> {
        private static final int MSG_HIDE_PREVIEW = 0;

        public UiHandler(SuggestionsView outerInstance) {
        @Override
        public void dispatchMessage(Message msg) {
            final SuggestionsView suggestionsView = getOuterInstance();
            switch (msg.what) {
            case MSG_HIDE_PREVIEW:
                suggestionsView.hidePreview();

        public void cancelHidePreview() {
            removeMessages(MSG_HIDE_PREVIEW);
        }

        public void cancelAllMessages() {
            cancelHidePreview();
        }
    private static class SuggestionsViewParams {
        private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
        private static final int DEFAULT_CENTER_SUGGESTION_PERCENTILE = 40;
        private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
        private static final int PUNCTUATIONS_IN_STRIP = 5;
        public final int mPadding;
        public final int mDividerWidth;
        public final int mSuggestionsStripHeight;
        public final int mSuggestionsCountInStrip;
        public final int mMaxMoreSuggestionsRow;
        public final float mMinMoreSuggestionsWidth;
        public final int mMoreSuggestionsBottomGap;
        private final List<TextView> mWords;
        private final List<View> mDividers;
        private final List<TextView> mInfos;
        private final int mColorValidTypedWord;
        private final int mColorTypedWord;
        private final int mColorAutoCorrect;
        private final int mColorSuggested;
        private final float mAlphaObsoleted;
        private final float mCenterSuggestionWeight;
        private final int mCenterSuggestionIndex;
        private final Drawable mMoreSuggestionsHint;
        private static final String MORE_SUGGESTIONS_HINT = "\u2026";

        private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
        private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
        private static final int AUTO_CORRECT_BOLD = 0x01;
        private static final int AUTO_CORRECT_UNDERLINE = 0x02;
        private static final int VALID_TYPED_WORD_BOLD = 0x04;
        private final int mSuggestionStripOption;
        private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
        public boolean mMoreSuggestionsAvailable;

        public final TextView mWordToSaveView;
        private final TextView mHintToSaveView;

        public SuggestionsViewParams(Context context, AttributeSet attrs, int defStyle,
                List<TextView> words, List<View> dividers, List<TextView> infos) {
            mWords = words;
            mDividers = dividers;
            mInfos = infos;

            final TextView word = words.get(0);
            final View divider = dividers.get(0);
            mPadding = word.getCompoundPaddingLeft() + word.getCompoundPaddingRight();
            divider.measure(
                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            mDividerWidth = divider.getMeasuredWidth();

            final Resources res = word.getResources();
            mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);

            final TypedArray a = context.obtainStyledAttributes(
                    attrs, R.styleable.SuggestionsView, defStyle, R.style.SuggestionsViewStyle);
            mSuggestionStripOption = a.getInt(R.styleable.SuggestionsView_suggestionStripOption, 0);
            final float alphaValidTypedWord = getPercent(a,
                    R.styleable.SuggestionsView_alphaValidTypedWord, 100);
            final float alphaTypedWord = getPercent(a,
                    R.styleable.SuggestionsView_alphaTypedWord, 100);
            final float alphaAutoCorrect = getPercent(a,
                    R.styleable.SuggestionsView_alphaAutoCorrect, 100);
            final float alphaSuggested = getPercent(a,
                    R.styleable.SuggestionsView_alphaSuggested, 100);
            mAlphaObsoleted = getPercent(a, R.styleable.SuggestionsView_alphaSuggested, 100);
            mColorValidTypedWord = applyAlpha(
                    a.getColor(R.styleable.SuggestionsView_colorValidTypedWord, 0),
                    alphaValidTypedWord);
            mColorTypedWord = applyAlpha(
                    a.getColor(R.styleable.SuggestionsView_colorTypedWord, 0), alphaTypedWord);
            mColorAutoCorrect = applyAlpha(
                    a.getColor(R.styleable.SuggestionsView_colorAutoCorrect, 0), alphaAutoCorrect);
            mColorSuggested = applyAlpha(
                    a.getColor(R.styleable.SuggestionsView_colorSuggested, 0), alphaSuggested);
            mSuggestionsCountInStrip = a.getInt(
                    R.styleable.SuggestionsView_suggestionsCountInStrip,
                    DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
            mCenterSuggestionWeight = getPercent(a,
                    R.styleable.SuggestionsView_centerSuggestionPercentile,
                    DEFAULT_CENTER_SUGGESTION_PERCENTILE);
            mMaxMoreSuggestionsRow = a.getInt(
                    R.styleable.SuggestionsView_maxMoreSuggestionsRow,
                    DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
            mMinMoreSuggestionsWidth = getRatio(a,
                    R.styleable.SuggestionsView_minMoreSuggestionsWidth);
            a.recycle();

            mMoreSuggestionsHint = getMoreSuggestionsHint(res,
                    res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect);
            mCenterSuggestionIndex = mSuggestionsCountInStrip / 2;
            mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
                    R.dimen.more_suggestions_bottom_gap);
            final LayoutInflater inflater = LayoutInflater.from(context);
            mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
            mHintToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
        private static Drawable getMoreSuggestionsHint(Resources res, float textSize, int color) {
            final Paint paint = new Paint();
            paint.setAntiAlias(true);
            paint.setTextAlign(Align.CENTER);
            paint.setTextSize(textSize);
            paint.setColor(color);
            final Rect bounds = new Rect();
Ken Wakasa's avatar
Ken Wakasa committed
            paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds);
            final int width = Math.round(bounds.width() + 0.5f);
            final int height = Math.round(bounds.height() + 0.5f);
            final Bitmap buffer = Bitmap.createBitmap(
                    width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
            final Canvas canvas = new Canvas(buffer);
            canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint);
            return new BitmapDrawable(res, buffer);
        }

        // Read integer value in TypedArray as percent.
        private static float getPercent(TypedArray a, int index, int defValue) {
            return a.getInt(index, defValue) / 100.0f;
        }

        // Read fraction value in TypedArray as float.
        private static float getRatio(TypedArray a, int index) {
            return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
        }

        private CharSequence getStyledSuggestionWord(SuggestedWords suggestions, int pos) {
            final CharSequence word = suggestions.getWord(pos);
            final boolean isAutoCorrect = pos == 1 && Utils.willAutoCorrect(suggestions);
            final boolean isTypedWordValid = pos == 0 && suggestions.mTypedWordValid;
            if (!isAutoCorrect && !isTypedWordValid)
                return word;
            final int len = word.length();
            final Spannable spannedWord = new SpannableString(word);
            final int option = mSuggestionStripOption;
            if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0)
                    || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) {
                spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            }
            if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) {
                spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            return spannedWord;
        }

        private int getWordPosition(int index, SuggestedWords suggestions) {
            // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
            // suggestions.
            final int centerPos = Utils.willAutoCorrect(suggestions) ? 1 : 0;
            if (index == mCenterSuggestionIndex) {
                return centerPos;
            } else if (index == centerPos) {
                return mCenterSuggestionIndex;
        private int getSuggestionTextColor(int index, SuggestedWords suggestions, int pos) {
            // TODO: Need to revisit this logic with bigram suggestions
            final boolean isSuggested = (pos != 0);

            final int color;
            if (index == mCenterSuggestionIndex && Utils.willAutoCorrect(suggestions)) {
                color = mColorAutoCorrect;
            } else if (index == mCenterSuggestionIndex && suggestions.mTypedWordValid) {
                color = mColorValidTypedWord;
            } else if (isSuggested) {
                color = mColorSuggested;
            } else {
                color = mColorTypedWord;
            }
satok's avatar
satok committed
            if (LatinImeLogger.sDBG) {
                if (index == mCenterSuggestionIndex && suggestions.mHasAutoCorrectionCandidate
                        && suggestions.shouldBlockAutoCorrection()) {
                    return 0xFFFF0000;
                }
            }

            final SuggestedWordInfo info = (pos < suggestions.size())
                    ? suggestions.getInfo(pos) : null;
            if (info != null && info.isObsoleteSuggestedWord()) {
                return applyAlpha(color, mAlphaObsoleted);
            } else {
                return color;
            }
        }

        private static int applyAlpha(final int color, final float alpha) {
            final int newAlpha = (int)(Color.alpha(color) * alpha);
            return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
        }

        private static void addDivider(final ViewGroup stripView, final View divider) {
            stripView.addView(divider);
            final LinearLayout.LayoutParams params =
                    (LinearLayout.LayoutParams)divider.getLayoutParams();
            params.gravity = Gravity.CENTER;
        }

        public void layout(SuggestedWords suggestions, ViewGroup stripView, ViewGroup placer,
            if (suggestions.isPunctuationSuggestions()) {
                layoutPunctuationSuggestions(suggestions, stripView);
                return;
            final int countInStrip = mSuggestionsCountInStrip;
            setupTexts(suggestions, countInStrip);
            mMoreSuggestionsAvailable = (suggestions.size() > countInStrip);
            int x = 0;
            for (int index = 0; index < countInStrip; index++) {
                final int pos = getWordPosition(index, suggestions);

                if (index != 0) {
                    final View divider = mDividers.get(pos);
                    // Add divider if this isn't the left most suggestion in suggestions strip.
                    addDivider(stripView, divider);
                    x += divider.getMeasuredWidth();
                final CharSequence styled = mTexts.get(pos);
                final TextView word = mWords.get(pos);
                if (index == mCenterSuggestionIndex && mMoreSuggestionsAvailable) {
                    // TODO: This "more suggestions hint" should have nicely designed icon.
                    word.setCompoundDrawablesWithIntrinsicBounds(
                            null, null, null, mMoreSuggestionsHint);
                    // HACK: To align with other TextView that has no compound drawables.
                    word.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
                    word.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
                // Disable this suggestion if the suggestion is null or empty.
                word.setEnabled(!TextUtils.isEmpty(styled));
                word.setTextColor(getSuggestionTextColor(index, suggestions, pos));
                final int width = getSuggestionWidth(index, stripWidth);
                final CharSequence text = getEllipsizedText(styled, width, word.getPaint());
                final float scaleX = word.getTextScaleX();
                word.setText(text); // TextView.setText() resets text scale x to 1.0.
                word.setTextScaleX(scaleX);
                stripView.addView(word);
                setLayoutWeight(
                        word, getSuggestionWeight(index), ViewGroup.LayoutParams.MATCH_PARENT);
                x += word.getMeasuredWidth();
                    final CharSequence debugInfo = getDebugInfo(suggestions, pos);
                    if (debugInfo != null) {
                        final TextView info = mInfos.get(pos);
                        info.setText(debugInfo);
                        info.measure(ViewGroup.LayoutParams.WRAP_CONTENT,
                                ViewGroup.LayoutParams.WRAP_CONTENT);
                        final int infoWidth = info.getMeasuredWidth();
                        final int y = info.getMeasuredHeight();
                        FrameLayoutCompatUtils.placeViewAt(
                                info, x - infoWidth, y, infoWidth, info.getMeasuredHeight());
        private int getSuggestionWidth(int index, int maxWidth) {
            final int paddings = mPadding * mSuggestionsCountInStrip;
            final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
            final int availableWidth = maxWidth - paddings - dividers;
            return (int)(availableWidth * getSuggestionWeight(index));
        private float getSuggestionWeight(int index) {
            if (index == mCenterSuggestionIndex) {
                return mCenterSuggestionWeight;
            } else {
                // TODO: Revisit this for cases of 5 or more suggestions
                return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1);
        private void setupTexts(SuggestedWords suggestions, int countInStrip) {
            mTexts.clear();
            final int count = Math.min(suggestions.size(), countInStrip);
            for (int pos = 0; pos < count; pos++) {
                final CharSequence styled = getStyledSuggestionWord(suggestions, pos);
                mTexts.add(styled);
            }
            for (int pos = count; pos < countInStrip; pos++) {
                // Make this inactive for touches in layout().
                mTexts.add(null);
            }
        private void layoutPunctuationSuggestions(SuggestedWords suggestions, ViewGroup stripView) {
            final int countInStrip = Math.min(suggestions.size(), PUNCTUATIONS_IN_STRIP);
            for (int index = 0; index < countInStrip; index++) {
                if (index != 0) {
                    // Add divider if this isn't the left most suggestion in suggestions strip.
                    addDivider(stripView, mDividers.get(index));
                }

                final TextView word = mWords.get(index);
                word.setEnabled(true);
                word.setTextColor(mColorAutoCorrect);
                final CharSequence text = suggestions.getWord(index);
                word.setText(text);
                word.setTextScaleX(1.0f);
                word.setCompoundDrawables(null, null, null, null);
                stripView.addView(word);
                setLayoutWeight(word, 1.0f, mSuggestionsStripHeight);
            mMoreSuggestionsAvailable = false;

        public void layoutAddToDictionaryHint(CharSequence word, ViewGroup stripView,
                int stripWidth, CharSequence hintText) {
            final int width = stripWidth - mDividerWidth - mPadding * 2;

            final TextView wordView = mWordToSaveView;
            wordView.setTextColor(mColorTypedWord);
            final int wordWidth = (int)(width * mCenterSuggestionWeight);
            final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
            final float wordScaleX = wordView.getTextScaleX();
            wordView.setTag(word);
            wordView.setText(text);
            wordView.setTextScaleX(wordScaleX);
            stripView.addView(wordView);
            setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);

            stripView.addView(mDividers.get(0));

            final TextView hintView = mHintToSaveView;
            hintView.setTextColor(mColorAutoCorrect);
            final int hintWidth = width - wordWidth;
            final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
            hintView.setText(hintText);
            hintView.setTextScaleX(hintScaleX);
            stripView.addView(hintView);
            setLayoutWeight(
                    hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);

        private static CharSequence getDebugInfo(SuggestedWords suggestions, int pos) {
            if (DBG && pos < suggestions.size()) {
                final SuggestedWordInfo wordInfo = suggestions.getInfo(pos);
                if (wordInfo != null) {
                    final CharSequence debugInfo = wordInfo.getDebugString();
                    if (!TextUtils.isEmpty(debugInfo)) {
                        return debugInfo;
                    }
                }
            }
            return null;
        }

        private static void setLayoutWeight(View v, float weight, int height) {
            final ViewGroup.LayoutParams lp = v.getLayoutParams();
            if (lp instanceof LinearLayout.LayoutParams) {
                final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
                llp.weight = weight;
                llp.width = 0;
                llp.height = height;
            }
        }

        private static float getTextScaleX(CharSequence text, int maxWidth, TextPaint paint) {
            paint.setTextScaleX(1.0f);
            final int width = getTextWidth(text, paint);
            if (width <= maxWidth) {
                return 1.0f;
            }
            return maxWidth / (float)width;
        }

        private static CharSequence getEllipsizedText(CharSequence text, int maxWidth,
                TextPaint paint) {
            if (text == null) return null;
            paint.setTextScaleX(1.0f);
            final int width = getTextWidth(text, paint);
            if (width <= maxWidth) {
                return text;
            }
            final float scaleX = maxWidth / (float)width;
            if (scaleX >= MIN_TEXT_XSCALE) {
                paint.setTextScaleX(scaleX);
                return text;
            }

            // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To
            // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
            final CharSequence ellipsized = TextUtils.ellipsize(
                    text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE);
            paint.setTextScaleX(MIN_TEXT_XSCALE);
            return ellipsized;
        }

        private static int getTextWidth(CharSequence text, TextPaint paint) {
            if (TextUtils.isEmpty(text)) return 0;
            final Typeface savedTypeface = paint.getTypeface();
            paint.setTypeface(getTextTypeface(text));
            final int len = text.length();
            final float[] widths = new float[len];
            final int count = paint.getTextWidths(text, 0, len, widths);
            int width = 0;
            for (int i = 0; i < count; i++) {
                width += Math.round(widths[i] + 0.5f);
            }
            paint.setTypeface(savedTypeface);
            return width;
        }

        private static Typeface getTextTypeface(CharSequence text) {
            if (!(text instanceof SpannableString))
                return Typeface.DEFAULT;

            final SpannableString ss = (SpannableString)text;
            final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class);
            if (styles.length == 0)
                return Typeface.DEFAULT;

            switch (styles[0].getStyle()) {
            case Typeface.BOLD: return Typeface.DEFAULT_BOLD;
            // TODO: BOLD_ITALIC, ITALIC case?
            default: return Typeface.DEFAULT;
            }
        }
     * Construct a {@link SuggestionsView} for showing suggested words for completion.
    public SuggestionsView(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.suggestionsViewStyle);
    public SuggestionsView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        final LayoutInflater inflater = LayoutInflater.from(context);
        inflater.inflate(R.layout.suggestions_strip, this);

        mPreviewPopup = new PopupWindow(context);
        mPreviewText = (TextView) inflater.inflate(R.layout.suggestion_preview, null);
        mPreviewPopup.setWindowLayoutMode(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        mPreviewPopup.setContentView(mPreviewText);
        mPreviewPopup.setBackgroundDrawable(null);

        mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip);
        for (int pos = 0; pos < MAX_SUGGESTIONS; pos++) {
            final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null);
            word.setTag(pos);
            word.setOnClickListener(this);
            word.setOnLongClickListener(this);
            mWords.add(word);
            final View divider = inflater.inflate(R.layout.suggestion_divider, null);
            divider.setTag(pos);
            divider.setOnClickListener(this);
            mDividers.add(divider);
            mInfos.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
        mParams = new SuggestionsViewParams(context, attrs, defStyle, mWords, mDividers, mInfos);
        mParams.mWordToSaveView.setOnClickListener(this);

        mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
        mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
                .findViewById(R.id.more_suggestions_view);
        mMoreSuggestionsBuilder = new MoreSuggestions.Builder(mMoreSuggestionsView);

        final PopupWindow moreWindow = new PopupWindow(context);
        moreWindow.setWindowLayoutMode(
                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        moreWindow.setBackgroundDrawable(new ColorDrawable(android.R.color.transparent));
        moreWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
        moreWindow.setFocusable(true);
        moreWindow.setOutsideTouchable(true);
        moreWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
            @Override
            public void onDismiss() {
                mKeyboardView.dimEntireKeyboard(false);
            }
        });
        mMoreSuggestionsWindow = moreWindow;

        final Resources res = context.getResources();
        mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset(
                R.dimen.more_suggestions_modal_tolerance);
        mMoreSuggestionsSlidingDetector = new GestureDetector(
                context, mMoreSuggestionsSlidingListener);
     * A connection back to the input method.
    public void setListener(Listener listener, View inputView) {
        mListener = listener;
        mKeyboardView = (KeyboardView)inputView.findViewById(R.id.keyboard_view);
    public void setSuggestions(SuggestedWords suggestions) {
        if (suggestions == null || suggestions.size() == 0)
        mSuggestions = suggestions;
        mParams.layout(mSuggestions, mSuggestionsStrip, this, getWidth());
    public boolean isShowingAddToDictionaryHint() {
        return mSuggestionsStrip.getChildCount() > 0
                && mSuggestionsStrip.getChildAt(0) == mParams.mWordToSaveView;
    public void showAddToDictionaryHint(CharSequence word, CharSequence hintText) {
        clear();
        mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth(), hintText);
    public boolean dismissAddToDictionaryHint() {
        if (isShowingAddToDictionaryHint()) {
            clear();
            return true;
        }
        return false;
    public SuggestedWords getSuggestions() {
        mSuggestionsStrip.removeAllViews();
        removeAllViews();
        addView(mSuggestionsStrip);
        dismissMoreSuggestions();
        mPreviewPopup.dismiss();

    private void addToDictionary(CharSequence word) {
        mListener.addWordToDictionary(word.toString());
    private final KeyboardActionListener mMoreSuggestionsListener =
            new KeyboardActionListener.Adapter() {
        @Override
        public boolean onCustomRequest(int requestCode) {
            final int index = requestCode;
            final CharSequence word = mSuggestions.getWord(index);
            mListener.pickSuggestionManually(index, word);
            dismissMoreSuggestions();
            return true;
        }

        @Override
        public void onCancelInput() {
            dismissMoreSuggestions();
        }
    };

    private final MoreKeysPanel.Controller mMoreSuggestionsController =
            new MoreKeysPanel.Controller() {
        @Override
        public boolean dismissMoreKeysPanel() {
            return dismissMoreSuggestions();
    private boolean dismissMoreSuggestions() {
        if (mMoreSuggestionsWindow.isShowing()) {
            mMoreSuggestionsWindow.dismiss();
            return true;
        }
        return false;
    }

    public boolean handleBack() {
        return dismissMoreSuggestions();
    }

    @Override
    public boolean onLongClick(View view) {
        return showMoreSuggestions();
    }

    private boolean showMoreSuggestions() {
        final SuggestionsViewParams params = mParams;
        if (params.mMoreSuggestionsAvailable) {
            final int stripWidth = getWidth();
            final View container = mMoreSuggestionsContainer;
            final int maxWidth = stripWidth - container.getPaddingLeft()
                    - container.getPaddingRight();
            final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder;
            builder.layout(mSuggestions, params.mSuggestionsCountInStrip, maxWidth,
                    (int)(maxWidth * params.mMinMoreSuggestionsWidth),
                    params.mMaxMoreSuggestionsRow);
            mMoreSuggestionsView.setKeyboard(builder.build());
            container.measure(
                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

            final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
            final int pointX = stripWidth / 2;
            final int pointY = -params.mMoreSuggestionsBottomGap;
            moreKeysPanel.showMoreKeysPanel(
                    this, mMoreSuggestionsController, pointX, pointY,
                    mMoreSuggestionsWindow, mMoreSuggestionsListener);
            mMoreSuggestionsMode = MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING;
            mOriginX = mLastX;
            mOriginY = mLastY;
            mKeyboardView.dimEntireKeyboard(true);
            for (int i = 0; i < params.mSuggestionsCountInStrip; i++) {
                mWords.get(i).setPressed(false);
            }
    // Working variables for onLongClick and dispatchTouchEvent.
    private int mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE;
    private static final int MORE_SUGGESTIONS_IN_MODAL_MODE = 0;
    private static final int MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING = 1;
    private static final int MORE_SUGGESTIONS_IN_SLIDING_MODE = 2;
    private int mLastX;
    private int mLastY;
    private int mOriginX;
    private int mOriginY;
    private final int mMoreSuggestionsModalTolerance;
    private final GestureDetector mMoreSuggestionsSlidingDetector;
    private final GestureDetector.OnGestureListener mMoreSuggestionsSlidingListener =
            new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onScroll(MotionEvent down, MotionEvent me, float deltaX, float deltaY) {
            final float dy = me.getY() - down.getY();
            if (deltaY > 0 && dy < 0) {
                return showMoreSuggestions();
            }
            return false;
        }
    };
    public boolean dispatchTouchEvent(MotionEvent me) {
        if (!mMoreSuggestionsWindow.isShowing()
                || mMoreSuggestionsMode == MORE_SUGGESTIONS_IN_MODAL_MODE) {
            mLastX = (int)me.getX();
            mLastY = (int)me.getY();
            if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) {
                return true;
            }
            return super.dispatchTouchEvent(me);
        }

        final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
        final int action = me.getAction();
        final long eventTime = me.getEventTime();
        final int index = me.getActionIndex();
        final int id = me.getPointerId(index);
        final PointerTracker tracker = PointerTracker.getPointerTracker(id, moreKeysPanel);
        final int x = (int)me.getX(index);
        final int y = (int)me.getY(index);
        final int translatedX = moreKeysPanel.translateX(x);
        final int translatedY = moreKeysPanel.translateY(y);

        if (mMoreSuggestionsMode == MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING) {
            if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance
                    || mOriginY - y >= mMoreSuggestionsModalTolerance) {
                // Decided to be in the sliding input mode only when the touch point has been moved
                // upward.
                mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_SLIDING_MODE;
                tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
            } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
                // Decided to be in the modal input mode
                mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE;
        // MORE_SUGGESTIONS_IN_SLIDING_MODE
        tracker.processMotionEvent(action, translatedX, translatedY, eventTime, moreKeysPanel);
        return true;
    }

    @Override
    public void onClick(View view) {
        if (view == mParams.mWordToSaveView) {
            addToDictionary((CharSequence)view.getTag());
        final Object tag = view.getTag();
        if (!(tag instanceof Integer))
            return;
        final int index = (Integer) tag;
        if (index >= mSuggestions.size())
            return;
        final CharSequence word = mSuggestions.getWord(index);
        mListener.pickSuggestionManually(index, word);
    protected void onDetachedFromWindow() {
        mHandler.cancelAllMessages();