Skip to content
Snippets Groups Projects
SuggestionStripView.java 38 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.suggestions;
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.Drawable;
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.RelativeLayout;
import com.android.inputmethod.keyboard.Keyboard;
import com.android.inputmethod.keyboard.KeyboardSwitcher;
import com.android.inputmethod.keyboard.MainKeyboardView;
import com.android.inputmethod.keyboard.MoreKeysPanel;
import com.android.inputmethod.keyboard.ViewLayoutUtils;
import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
import com.android.inputmethod.latin.AutoCorrection;
import com.android.inputmethod.latin.CollectionUtils;
import com.android.inputmethod.latin.Constants;
import com.android.inputmethod.latin.LatinImeLogger;
import com.android.inputmethod.latin.R;
import com.android.inputmethod.latin.ResourceUtils;
import com.android.inputmethod.latin.SuggestedWords;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
Jean Chalard's avatar
Jean Chalard committed
import com.android.inputmethod.latin.Utils;
import com.android.inputmethod.latin.define.ProductionFlag;
import com.android.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestionsListener;
Kurt Partridge's avatar
Kurt Partridge committed
import com.android.inputmethod.research.ResearchLogger;
import java.util.ArrayList;

public final class SuggestionStripView extends RelativeLayout implements OnClickListener,
        OnLongClickListener {
    public interface Listener {
        public void addWordToUserDictionary(String word);
        public void pickSuggestionManually(int index, SuggestedWordInfo word);
    // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
    public static final int MAX_SUGGESTIONS = 18;
    static final boolean DBG = LatinImeLogger.sDBG;
    private final ViewGroup mSuggestionsStrip;
    MainKeyboardView mMainKeyboardView;
    private final View mMoreSuggestionsContainer;
    private final MoreSuggestionsView mMoreSuggestionsView;
    private final MoreSuggestions.Builder mMoreSuggestionsBuilder;

    private final ArrayList<TextView> mWords = CollectionUtils.newArrayList();
    private final ArrayList<TextView> mInfos = CollectionUtils.newArrayList();
    private final ArrayList<View> mDividers = CollectionUtils.newArrayList();
    private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
    private final SuggestionStripViewParams mParams;
    private static final float MIN_TEXT_XSCALE = 0.70f;
    private static final class SuggestionStripViewParams {
        private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
        private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f;
        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 mMoreSuggestionsRowHeight;
        private int mMaxMoreSuggestionsRow;
        public final int mMoreSuggestionsBottomGap;
        private final ArrayList<TextView> mWords;
        private final ArrayList<View> mDividers;
        private final ArrayList<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 String LEFTWARDS_ARROW = "\u2190";

        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 = CollectionUtils.newArrayList();
        public boolean mMoreSuggestionsAvailable;

        private final TextView mWordToSaveView;
        private final TextView mLeftwardsArrowView;
        private final TextView mHintToSaveView;

        public SuggestionStripViewParams(final Context context, final AttributeSet attrs,
                final int defStyle, final ArrayList<TextView> words, final ArrayList<View> dividers,
                final ArrayList<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.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle);
            mSuggestionStripOption = a.getInt(
                    R.styleable.SuggestionStripView_suggestionStripOption, 0);
            final float alphaValidTypedWord = ResourceUtils.getFraction(a,
                    R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f);
            final float alphaTypedWord = ResourceUtils.getFraction(a,
                    R.styleable.SuggestionStripView_alphaTypedWord, 1.0f);
            final float alphaAutoCorrect = ResourceUtils.getFraction(a,
                    R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f);
            final float alphaSuggested = ResourceUtils.getFraction(a,
                    R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
            mAlphaObsoleted = ResourceUtils.getFraction(a,
                    R.styleable.SuggestionStripView_alphaSuggested, 1.0f);
            mColorValidTypedWord = applyAlpha(a.getColor(
                    R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord);
            mColorTypedWord = applyAlpha(a.getColor(
                    R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord);
            mColorAutoCorrect = applyAlpha(a.getColor(
                    R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect);
            mColorSuggested = applyAlpha(a.getColor(
                    R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested);
            mSuggestionsCountInStrip = a.getInt(
                    R.styleable.SuggestionStripView_suggestionsCountInStrip,
                    DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
            mCenterSuggestionWeight = ResourceUtils.getFraction(a,
                    R.styleable.SuggestionStripView_centerSuggestionPercentile,
                    DEFAULT_CENTER_SUGGESTION_PERCENTILE);
                    R.styleable.SuggestionStripView_maxMoreSuggestionsRow,
            mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a,
                    R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f);
            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);
            mMoreSuggestionsRowHeight = res.getDimensionPixelSize(
                    R.dimen.more_suggestions_row_height);
            final LayoutInflater inflater = LayoutInflater.from(context);
            mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
            mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
            mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
        public int getMaxMoreSuggestionsRow() {
            return mMaxMoreSuggestionsRow;
        }

        private int getMoreSuggestionsHeight() {
            return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap;
        }

        public int setMoreSuggestionsHeight(final int remainingHeight) {
            final int currentHeight = getMoreSuggestionsHeight();
            if (currentHeight <= remainingHeight) {
                return currentHeight;
            }

            mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap)
                    / mMoreSuggestionsRowHeight;
            final int newHeight = getMoreSuggestionsHeight();
            return newHeight;
        }

        private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize,
                final 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);
        }

        private CharSequence getStyledSuggestionWord(final SuggestedWords suggestedWords,
                final int indexInSuggestedWords) {
            final String word = suggestedWords.getWord(indexInSuggestedWords);
            final boolean isAutoCorrect = indexInSuggestedWords == 1
                    && suggestedWords.willAutoCorrect();
            final boolean isTypedWordValid = indexInSuggestedWords == 0
                    && suggestedWords.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 getIndexInSuggestedWords(final int indexInStrip,
                final SuggestedWords suggestedWords) {
            // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
            // suggestions.
            final int mostImportantIndexInSuggestedWords = suggestedWords.willAutoCorrect() ? 1 : 0;
            if (indexInStrip == mCenterSuggestionIndex) {
                return mostImportantIndexInSuggestedWords;
            } else if (indexInStrip == mostImportantIndexInSuggestedWords) {
                return mCenterSuggestionIndex;
        private int getSuggestionTextColor(final int indexInStrip,
                final SuggestedWords suggestedWords) {
            final int indexInSuggestedWords = getIndexInSuggestedWords(
                    indexInStrip, suggestedWords);
            // TODO: Need to revisit this logic with bigram suggestions
            final boolean isSuggested = (indexInSuggestedWords != 0);

            final int color;
            if (indexInStrip == mCenterSuggestionIndex && suggestedWords.willAutoCorrect()) {
                color = mColorAutoCorrect;
            } else if (indexInStrip == mCenterSuggestionIndex && suggestedWords.mTypedWordValid) {
                color = mColorValidTypedWord;
            } else if (isSuggested) {
                color = mColorSuggested;
            } else {
                color = mColorTypedWord;
            }
Jean Chalard's avatar
Jean Chalard committed
            if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
                // If we auto-correct, then the autocorrection is in slot 0 and the typed word
                // is in slot 1.
                if (indexInStrip == mCenterSuggestionIndex
                        && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
                                suggestedWords.getWord(1), suggestedWords.getWord(0))) {
satok's avatar
satok committed
                    return 0xFFFF0000;
                }
            }
            if (suggestedWords.mIsObsoleteSuggestions && isSuggested) {
                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(final SuggestedWords suggestedWords, final ViewGroup stripView,
                final ViewGroup placer, final int stripWidth) {
            if (suggestedWords.mIsPunctuationSuggestions) {
                layoutPunctuationSuggestions(suggestedWords, stripView);
            final int countInStrip = mSuggestionsCountInStrip;
            setupTexts(suggestedWords, countInStrip);
            mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
            int x = 0;
            for (int indexInStrip = 0; indexInStrip < countInStrip; indexInStrip++) {
                if (indexInStrip != 0) {
                    final View divider = mDividers.get(indexInStrip);
                    // Add divider if this isn't the left most suggestion in suggestions strip.
                    addDivider(stripView, divider);
                    x += divider.getMeasuredWidth();
                final int width = getSuggestionWidth(indexInStrip, stripWidth);
                final TextView word = layoutWord(suggestedWords, indexInStrip, width);
                stripView.addView(word);
                setLayoutWeight(word, getSuggestionWeight(indexInStrip),
                        ViewGroup.LayoutParams.MATCH_PARENT);
                x += word.getMeasuredWidth();
                if (DBG) {
                    layoutDebugInfo(suggestedWords, indexInStrip, placer, x);
        /**
         * Format appropriately the suggested word indirectly specified by
         * <code>indexInStrip</code> as text in a corresponding {@link TextView}. When the
         * suggested word doesn't exist, the corresponding {@link TextView} will be disabled
         * and never respond to user interaction. The suggested word may be shrunk or ellipsized to
         * fit in the specified width.
         *
         * The <code>indexInStrip</code> argument is the index in the suggestion strip. The indices
         * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
         * The index of the most important suggestion is in {@link #mCenterSuggestionIndex}. This
         * usually doesn't match the index in <code>suggedtedWords</code> -- see
         * {@link #getIndexInSuggestedWords(int,SuggestedWords)}.
         *
         * @param suggestedWords the list of suggestions.
         * @param indexInStrip the in the suggestion strip.
         * @param width the maximum width for layout in pixels.
         * @return the {@link TextView} containing the suggested word appropriately formatted.
         */
        private TextView layoutWord(final SuggestedWords suggestedWords, final int indexInStrip,
                final int width) {
            final int indexInSuggestedWords = getIndexInSuggestedWords(
                    indexInStrip, suggestedWords);
            final CharSequence styled = mTexts.get(indexInSuggestedWords);
            final TextView word = mWords.get(indexInSuggestedWords);
            if (indexInStrip == mCenterSuggestionIndex && mMoreSuggestionsAvailable) {
                // TODO: This "more suggestions hint" should have a nicely designed icon.
                word.setCompoundDrawablesWithIntrinsicBounds(
                        null, null, null, mMoreSuggestionsHint);
                // HACK: Align with other TextViews that have no compound drawables.
                word.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
            } else {
                word.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
            }

            // Disable this suggestion if the suggestion is null or empty.
            word.setEnabled(!TextUtils.isEmpty(styled));
            word.setTextColor(getSuggestionTextColor(indexInStrip, suggestedWords));
            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);
            return word;
        }

        private void layoutDebugInfo(final SuggestedWords suggestedWords, final int indexInStrip,
                final ViewGroup placer, final int x) {
            final int indexInSuggestedWords = getIndexInSuggestedWords(
                    indexInStrip, suggestedWords);
            if (indexInSuggestedWords >= suggestedWords.size()) {
                return;
            }
            final String debugInfo = Utils.getDebugInfo(suggestedWords, indexInSuggestedWords);
            if (debugInfo == null) {
                return;
            }
            final TextView info = mInfos.get(indexInSuggestedWords);
            info.setText(debugInfo);
            placer.addView(info);
            info.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            final int infoWidth = info.getMeasuredWidth();
            final int y = info.getMeasuredHeight();
            ViewLayoutUtils.placeViewAt(
                    info, x - infoWidth, y, infoWidth, info.getMeasuredHeight());
        }

        private int getSuggestionWidth(final int indexInStrip, final int maxWidth) {
            final int paddings = mPadding * mSuggestionsCountInStrip;
            final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
            final int availableWidth = maxWidth - paddings - dividers;
            return (int)(availableWidth * getSuggestionWeight(indexInStrip));
        private float getSuggestionWeight(final int indexInStrip) {
            if (indexInStrip == mCenterSuggestionIndex) {
                return mCenterSuggestionWeight;
            } else {
                // TODO: Revisit this for cases of 5 or more suggestions
                return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1);
        private void setupTexts(final SuggestedWords suggestedWords, final int countInStrip) {
            mTexts.clear();
            final int count = Math.min(suggestedWords.size(), countInStrip);
            for (int pos = 0; pos < count; pos++) {
                final CharSequence styled = getStyledSuggestionWord(suggestedWords, pos);
                mTexts.add(styled);
            }
            for (int pos = count; pos < countInStrip; pos++) {
                // Make this inactive for touches in layout().
                mTexts.add(null);
            }
        private void layoutPunctuationSuggestions(final SuggestedWords suggestedWords,
                final ViewGroup stripView) {
            final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP);
            for (int indexInStrip = 0; indexInStrip < countInStrip; indexInStrip++) {
                if (indexInStrip != 0) {
                    // Add divider if this isn't the left most suggestion in suggestions strip.
                    addDivider(stripView, mDividers.get(indexInStrip));
                final TextView word = mWords.get(indexInStrip);
                word.setEnabled(true);
                word.setTextColor(mColorAutoCorrect);
                final String text = suggestedWords.getWord(indexInStrip);
                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(final String word, final ViewGroup stripView,
                final int stripWidth, final CharSequence hintText, final OnClickListener listener) {
            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 leftArrowView = mLeftwardsArrowView;
            leftArrowView.setTextColor(mColorAutoCorrect);
            leftArrowView.setText(LEFTWARDS_ARROW);
            stripView.addView(leftArrowView);

            final TextView hintView = mHintToSaveView;
            hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
            hintView.setTextColor(mColorAutoCorrect);
            final int hintWidth = width - wordWidth - leftArrowView.getWidth();
            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);

            wordView.setOnClickListener(listener);
            leftArrowView.setOnClickListener(listener);
            hintView.setOnClickListener(listener);
        }

        public CharSequence getAddToDictionaryWord() {
            return (CharSequence)mWordToSaveView.getTag();
        }

        public boolean isAddToDictionaryShowing(final View v) {
            return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
        private static void setLayoutWeight(final View v, final float weight, final 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(final CharSequence text, final int maxWidth,
                final 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(final CharSequence text, final int maxWidth,
                final 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(final CharSequence text, final 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(final 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 SuggestionStripView} for showing suggestions to be picked by the user.
    public SuggestionStripView(final Context context, final AttributeSet attrs) {
        this(context, attrs, R.attr.suggestionStripViewStyle);
    public SuggestionStripView(final Context context, final AttributeSet attrs,
            final int defStyle) {
        super(context, attrs, defStyle);
        final LayoutInflater inflater = LayoutInflater.from(context);
        inflater.inflate(R.layout.suggestions_strip, this);
        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 SuggestionStripViewParams(
                context, attrs, defStyle, mWords, mDividers, mInfos);

        mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
        mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
                .findViewById(R.id.more_suggestions_view);
Tadashi G. Takaoka's avatar
Tadashi G. Takaoka committed
        mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView);
        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(final Listener listener, final View inputView) {
        mListener = listener;
        mMainKeyboardView = (MainKeyboardView)inputView.findViewById(R.id.keyboard_view);
    public void setSuggestions(final SuggestedWords suggestedWords) {
        mSuggestedWords = suggestedWords;
        mParams.layout(mSuggestedWords, mSuggestionsStrip, this, getWidth());
        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
            ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords);
    public int setMoreSuggestionsHeight(final int remainingHeight) {
        return mParams.setMoreSuggestionsHeight(remainingHeight);
    }
    public boolean isShowingAddToDictionaryHint() {
        return mSuggestionsStrip.getChildCount() > 0
                && mParams.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0));
    public void showAddToDictionaryHint(final String word, final CharSequence hintText) {
        clear();
        mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth(), hintText, this);
    public boolean dismissAddToDictionaryHint() {
        if (isShowingAddToDictionaryHint()) {
            clear();
            return true;
        }
        return false;
        mSuggestionsStrip.removeAllViews();
        removeAllViews();
        addView(mSuggestionsStrip);
        dismissMoreSuggestions();
    private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() {
        public void onSuggestionSelected(final int index, final SuggestedWordInfo wordInfo) {
            mListener.pickSuggestionManually(index, wordInfo);
            dismissMoreSuggestions();
        }

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

    private final MoreKeysPanel.Controller mMoreSuggestionsController =
            new MoreKeysPanel.Controller() {
        @Override
        public boolean onDismissMoreKeysPanel() {
            return mMainKeyboardView.onDismissMoreKeysPanel();
        public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
            mMainKeyboardView.onShowMoreKeysPanel(panel);

        @Override
        public void onCancelMoreKeysPanel() {
            dismissMoreSuggestions();
        }
    boolean dismissMoreSuggestions() {
        return mMoreSuggestionsView.dismissMoreKeysPanel();
    public boolean onLongClick(final View view) {
        AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
                Constants.NOT_A_CODE, this);
        final Keyboard parentKeyboard = KeyboardSwitcher.getInstance().getKeyboard();
        if (parentKeyboard == null) {
            return false;
        final SuggestionStripViewParams params = mParams;
        if (!params.mMoreSuggestionsAvailable) {
            return false;
        }
        final int stripWidth = getWidth();
        final View container = mMoreSuggestionsContainer;
        final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight();
        final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder;
        builder.layout(mSuggestedWords, params.mSuggestionsCountInStrip, maxWidth,
                (int)(maxWidth * params.mMinMoreSuggestionsWidth),
                params.getMaxMoreSuggestionsRow(), parentKeyboard);
        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,
                mMoreSuggestionsListener);
        mMoreSuggestionsMode = MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING;
        mOriginX = mLastX;
        mOriginY = mLastY;
        for (int i = 0; i < params.mSuggestionsCountInStrip; i++) {
            mWords.get(i).setPressed(false);
        }
        return true;
    // 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(final MotionEvent me) {
        if (!mMoreSuggestionsView.isShowingInParent()) {
            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 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;
            } 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;
                mMoreSuggestionsView.adjustVerticalCorrectionForModalMode();
        // MORE_SUGGESTIONS_IN_SLIDING_MODE
        mMoreSuggestionsView.processMotionEvent(action, translatedX, translatedY, id, eventTime);
        return true;
    }

    @Override
    public void onClick(final View view) {
        if (mParams.isAddToDictionaryShowing(view)) {
            mListener.addWordToUserDictionary(mParams.getAddToDictionaryWord().toString());
        final Object tag = view.getTag();
        if (!(tag instanceof Integer))
            return;
        final int index = (Integer) tag;
        if (index >= mSuggestedWords.size())
        final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index);
        mListener.pickSuggestionManually(index, wordInfo);
    protected void onDetachedFromWindow() {