Skip to content
Snippets Groups Projects
CandidateView.java 32.9 KiB
Newer Older
 * Copyright (C) 2010 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;
satok's avatar
satok committed
import android.graphics.Color;
import android.graphics.Typeface;
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.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;

import com.android.inputmethod.compat.FrameLayoutCompatUtils;
import com.android.inputmethod.compat.LinearLayoutCompatUtils;
import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;

import java.util.ArrayList;
satok's avatar
satok committed
import java.util.List;
public class CandidateView extends LinearLayout implements OnClickListener {
    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}.
    private static final int MAX_SUGGESTIONS = 18;
    private static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
    private static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
    private static final boolean DBG = LatinImeLogger.sDBG;
    private final ViewGroup mCandidatesStrip;
    private final ViewGroup mCandidatesPaneControl;
    private final TextView mExpandCandidatesPane;
    private final TextView mCloseCandidatesPane;
    private ViewGroup mCandidatesPane;
    private ViewGroup mCandidatesPaneContainer;
    private View mKeyboardView;
    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 int mCandidateStripHeight;
    private final PopupWindow mPreviewPopup;
    private final TextView mPreviewText;
    private final View mTouchToSave;
    private final TextView mWordToSave;

    private Listener mListener;
    private SuggestedWords mSuggestions = SuggestedWords.EMPTY;
    private boolean mShowingAutoCorrectionInverted;
    private boolean mShowingAddToDictionary;
    private final SuggestionsStripParams mStripParams;
    private final SuggestionsPaneParams mPaneParams;
    private static final float MIN_TEXT_XSCALE = 0.75f;
    private final UiHandler mHandler = new UiHandler(this);
    private static class UiHandler extends StaticInnerHandlerWrapper<CandidateView> {
        private static final int MSG_HIDE_PREVIEW = 0;
        private static final int MSG_UPDATE_SUGGESTION = 1;

        private static final long DELAY_HIDE_PREVIEW = 1000;
        private static final long DELAY_UPDATE_SUGGESTION = 300;

        public UiHandler(CandidateView outerInstance) {
            super(outerInstance);
        }

        @Override
        public void dispatchMessage(Message msg) {
            final CandidateView candidateView = getOuterInstance();
            switch (msg.what) {
            case MSG_HIDE_PREVIEW:
                candidateView.hidePreview();
            case MSG_UPDATE_SUGGESTION:
                candidateView.updateSuggestions();

        public void postHidePreview() {
            cancelHidePreview();
            sendMessageDelayed(obtainMessage(MSG_HIDE_PREVIEW), DELAY_HIDE_PREVIEW);
        }

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

        public void postUpdateSuggestions() {
            cancelUpdateSuggestions();
            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION),
                    DELAY_UPDATE_SUGGESTION);
        }

        public void cancelUpdateSuggestions() {
            removeMessages(MSG_UPDATE_SUGGESTION);
        }

        public void cancelAllMessages() {
            cancelHidePreview();
            cancelUpdateSuggestions();
        }
    private static class CandidateViewParams {
        public final int mPadding;
        public final int mDividerWidth;
        public final int mDividerHeight;
        public final int mControlWidth;

        protected CandidateViewParams(TextView word, View divider, View control) {
            mPadding = word.getCompoundPaddingLeft() + word.getCompoundPaddingRight();
            divider.measure(WRAP_CONTENT, MATCH_PARENT);
            mDividerWidth = divider.getMeasuredWidth();
            mDividerHeight = divider.getMeasuredHeight();
            mControlWidth = control.getMeasuredWidth();
        }
    }

    private static class SuggestionsPaneParams extends CandidateViewParams {
        public SuggestionsPaneParams(List<TextView> words, List<View> dividers, View control) {
            super(words.get(0), dividers.get(0), control);
        }
    }

    private static class SuggestionsStripParams extends CandidateViewParams {
        private static final int DEFAULT_CANDIDATE_COUNT_IN_STRIP = 3;
        private static final int PUNCTUATIONS_IN_STRIP = 6;

        private final int mColorTypedWord;
        private final int mColorAutoCorrect;
        private final int mColorSuggestedCandidate;
        private final int mCandidateCountInStrip;

        private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
        private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
        private final CharacterStyle mInvertedForegroundColorSpan;
        private final CharacterStyle mInvertedBackgroundColorSpan;
        private static final int AUTO_CORRECT_BOLD = 0x01;
        private static final int AUTO_CORRECT_UNDERLINE = 0x02;
        private static final int AUTO_CORRECT_INVERT = 0x04;

        public final TextPaint mPaint;
        private final int mAutoCorrectHighlight;

        private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
        private SuggestedWords mSuggestedWords;

        public int mCountInStrip;
        // True if the mCountInStrip suggestions can fit in suggestion strip in equally divided
        // width without squeezing the text.
        public boolean mCanUseFixedWidthColumns;
        public int mMaxWidth;
        public int mAvailableWidthForWords;
        public int mConstantWidthForPaddings;
        public int mVariableWidthForWords;
        public float mScaleX;

        public SuggestionsStripParams(Context context, AttributeSet attrs, int defStyle,
                List<TextView> words, List<View> dividers, View control) {
            super(words.get(0), dividers.get(0), control);
            final TypedArray a = context.obtainStyledAttributes(
                    attrs, R.styleable.CandidateView, defStyle, R.style.CandidateViewStyle);
            mAutoCorrectHighlight = a.getInt(R.styleable.CandidateView_autoCorrectHighlight, 0);
            mColorTypedWord = a.getColor(R.styleable.CandidateView_colorTypedWord, 0);
            mColorAutoCorrect = a.getColor(R.styleable.CandidateView_colorAutoCorrect, 0);
            mColorSuggestedCandidate = a.getColor(R.styleable.CandidateView_colorSuggested, 0);
            mCandidateCountInStrip = a.getInt(
                    R.styleable.CandidateView_candidateCountInStrip,
                    DEFAULT_CANDIDATE_COUNT_IN_STRIP);
            a.recycle();

            mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorTypedWord ^ 0x00ffffff);
            mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorTypedWord);

            mPaint = new TextPaint();
            final float textSize = context.getResources().getDimension(R.dimen.candidate_text_size);
            mPaint.setTextSize(textSize);
        }

        public CharSequence getWord(int pos) {
            return mTexts.get(pos);
        }

        public CharSequence getDebugInfo(int pos) {
            if (DBG) {
                final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(pos);
                if (wordInfo != null) {
                    final CharSequence debugInfo = wordInfo.getDebugString();
                    if (!TextUtils.isEmpty(debugInfo)) {
                        return debugInfo;
                    }
                }
            }
            return null;
        }

        public CharSequence getStyledCandidateWord(CharSequence word, boolean isAutoCorrect) {
            if (!isAutoCorrect)
                return word;
            final int len = word.length();
            final Spannable spannedWord = new SpannableString(word);
            if ((mAutoCorrectHighlight & AUTO_CORRECT_BOLD) != 0)
                spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            if ((mAutoCorrectHighlight & AUTO_CORRECT_UNDERLINE) != 0)
                spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            return spannedWord;
        }

        public int getWordPosition(int index) {
            if (index >= 2) {
                return index;
            }
            final boolean willAutoCorrect = !mSuggestedWords.mTypedWordValid
                    && mSuggestedWords.mHasMinimalSuggestion;
            return willAutoCorrect ? 1 - index : index;
        }

        public int getCandidateTextColor(int pos) {
            final SuggestedWords suggestions = mSuggestedWords;
            final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion
                    && ((pos == 1 && !suggestions.mTypedWordValid)
                            || (pos == 0 && suggestions.mTypedWordValid));
            // TODO: Need to revisit this logic with bigram suggestions
            final boolean isSuggestedCandidate = (pos != 0);
            final boolean isPunctuationSuggestions = suggestions.isPunctuationSuggestions();

            final int color;
            if (isPunctuationSuggestions) {
                color = mColorTypedWord;
            } else if (isAutoCorrect) {
                color = mColorAutoCorrect;
            } else if (isSuggestedCandidate) {
                color = mColorSuggestedCandidate;
            } else {
                color = mColorTypedWord;
            }
            final SuggestedWordInfo info = suggestions.getInfo(pos);
            if (info != null && info.isPreviousSuggestedWord()) {
                return applyAlpha(color, 0.5f);
            } 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));
        }

        public CharSequence getInvertedText(CharSequence text) {
            if ((mAutoCorrectHighlight & AUTO_CORRECT_INVERT) == 0)
                return null;
            final int len = text.length();
            final Spannable word = new SpannableString(text);
            word.setSpan(mInvertedBackgroundColorSpan, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            word.setSpan(mInvertedForegroundColorSpan, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            return word;
        }

        public void layoutStrip(SuggestedWords suggestions, int maxWidth) {
            mSuggestedWords = suggestions;
            final int maxCount = suggestions.isPunctuationSuggestions()
                    ? PUNCTUATIONS_IN_STRIP : mCandidateCountInStrip;
            final int size = suggestions.size();
            setupTexts(suggestions, size);
            mCountInStrip = Math.min(maxCount, size);
            mScaleX = 1.0f;

            do {
                mMaxWidth = maxWidth;
                if (size > mCountInStrip) {
                    mMaxWidth -= mControlWidth;
                }

                tryLayout();

                if (mCanUseFixedWidthColumns) {
                    return;
                }
                if (mVariableWidthForWords <= mAvailableWidthForWords) {
                    return;
                }

                final float scaleX = mAvailableWidthForWords / (float)mVariableWidthForWords;
                if (scaleX >= MIN_TEXT_XSCALE) {
                    mScaleX = scaleX;
                    return;
                }

                mCountInStrip--;
            } while (mCountInStrip > 1);
        }

        public void tryLayout() {
            final int maxCount = mCountInStrip;
            final int dividers = mDividerWidth * (maxCount - 1);
            mConstantWidthForPaddings = dividers + mPadding * maxCount;
            mAvailableWidthForWords = mMaxWidth - mConstantWidthForPaddings;

            mPaint.setTextScaleX(mScaleX);
            final int maxFixedWidthForWord = (mMaxWidth - dividers) / maxCount - mPadding;
            mCanUseFixedWidthColumns = true;
            mVariableWidthForWords = 0;
            for (int i = 0; i < maxCount; i++) {
                final int width = getTextWidth(mTexts.get(i), mPaint);
                if (width > maxFixedWidthForWord)
                    mCanUseFixedWidthColumns = false;
                mVariableWidthForWords += width;
            }
        }

        private void setupTexts(SuggestedWords suggestions, int count) {
            mTexts.clear();
            for (int i = 0; i < count; i++) {
                final CharSequence word = suggestions.getWord(i);
                final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion
                        && ((i == 1 && !suggestions.mTypedWordValid)
                                || (i == 0 && suggestions.mTypedWordValid));
                final CharSequence styled = getStyledCandidateWord(word, isAutoCorrect);
                mTexts.add(styled);
            }
        }

        @Override
        public String toString() {
            return String.format(
                    "count=%d width=%d avail=%d fixcol=%s scaleX=%4.2f const=%d var=%d",
                    mCountInStrip, mMaxWidth, mAvailableWidthForWords, mCanUseFixedWidthColumns,
                    mScaleX, mConstantWidthForPaddings, mVariableWidthForWords);
        }
    }

    /**
     * Construct a CandidateView for showing suggested words for completion.
     * @param context
     * @param attrs
     */
    public CandidateView(Context context, AttributeSet attrs) {
        this(context, attrs, R.attr.candidateViewStyle);
    }

    public CandidateView(Context context, AttributeSet attrs, int defStyle) {
        // Note: Up to version 10 (Gingerbread) of the API, LinearLayout doesn't have 3-argument
        // constructor.
        // TODO: Call 3-argument constructor, super(context, attrs, defStyle), when we abandon
        // backward compatibility with the version 10 or earlier of the API.
        super(context, attrs);
        if (defStyle != R.attr.candidateViewStyle) {
            throw new IllegalArgumentException(
                    "can't accept defStyle other than R.attr.candidayeViewStyle: defStyle="
                    + defStyle);
        }
        setBackgroundDrawable(LinearLayoutCompatUtils.getBackgroundDrawable(
                context, attrs, defStyle, R.style.CandidateViewStyle));
        Resources res = context.getResources();
        LayoutInflater inflater = LayoutInflater.from(context);
        inflater.inflate(R.layout.candidates_strip, this);

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

        mCandidatesStrip = (ViewGroup)findViewById(R.id.candidates_strip);
        mCandidateStripHeight = res.getDimensionPixelOffset(R.dimen.candidate_strip_height);
        for (int i = 0; i < MAX_SUGGESTIONS; i++) {
            final TextView word = (TextView)inflater.inflate(R.layout.candidate_word, null);
            word.setTag(i);
            word.setOnClickListener(this);
            mWords.add(word);
            final View divider = inflater.inflate(R.layout.candidate_divider, null);
            divider.setTag(i);
            divider.setOnClickListener(this);
            mDividers.add(divider);
            mInfos.add((TextView)inflater.inflate(R.layout.candidate_info, null));
        mTouchToSave = findViewById(R.id.touch_to_save);
        mWordToSave = (TextView)findViewById(R.id.word_to_save);
        mWordToSave.setOnClickListener(this);

        final TypedArray keyboardViewAttr = context.obtainStyledAttributes(
                attrs, R.styleable.KeyboardView, R.attr.keyboardViewStyle, R.style.KeyboardView);
        final Drawable expandBackground = keyboardViewAttr.getDrawable(
                R.styleable.KeyboardView_keyBackground);
        final Drawable closeBackground = keyboardViewAttr.getDrawable(
                R.styleable.KeyboardView_keyBackground);
        final int keyTextColor = keyboardViewAttr.getColor(
                R.styleable.KeyboardView_keyTextColor, 0xFF000000);
        keyboardViewAttr.recycle();

        mCandidatesPaneControl = (ViewGroup)findViewById(R.id.candidates_pane_control);
        mExpandCandidatesPane = (TextView)findViewById(R.id.expand_candidates_pane);
        mExpandCandidatesPane.setBackgroundDrawable(expandBackground);
        mExpandCandidatesPane.setTextColor(keyTextColor);
        mExpandCandidatesPane.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                expandCandidatesPane();
            }
        });
        mCloseCandidatesPane = (TextView)findViewById(R.id.close_candidates_pane);
        mCloseCandidatesPane.setBackgroundDrawable(closeBackground);
        mCloseCandidatesPane.setTextColor(keyTextColor);
        mCloseCandidatesPane.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                closeCandidatesPane();
            }
        });
        mCandidatesPaneControl.measure(WRAP_CONTENT, WRAP_CONTENT);
        mStripParams = new SuggestionsStripParams(context, attrs, defStyle,
                mWords, mDividers, mCandidatesPaneControl);
        mPaneParams = new SuggestionsPaneParams(mWords, mDividers, mCandidatesPaneControl);
     * A connection back to the input method.
    public void setListener(Listener listener, View inputView) {
        mListener = listener;
        mKeyboardView = inputView.findViewById(R.id.keyboard_view);
        mCandidatesPane = FrameLayoutCompatUtils.getPlacer(
                (ViewGroup)inputView.findViewById(R.id.candidates_pane));
        mCandidatesPane.setOnClickListener(this);
        mCandidatesPaneContainer = (ViewGroup)inputView.findViewById(
                R.id.candidates_pane_container);
    public void setSuggestions(SuggestedWords suggestions) {
        if (suggestions == null)
        mSuggestions = suggestions;
        mExpandCandidatesPane.setEnabled(false);
        if (mShowingAutoCorrectionInverted) {
            mHandler.postUpdateSuggestions();
    private void updateSuggestions() {
        closeCandidatesPane();
        final SuggestedWords suggestions = mSuggestions;
        if (suggestions.size() == 0)
            return;

        final int paneWidth = getWidth();
        final SuggestionsStripParams stripParams = mStripParams;
        final SuggestionsPaneParams paneParams = mPaneParams;
        stripParams.layoutStrip(suggestions, paneWidth);

        final int count = Math.min(mWords.size(), suggestions.size());
        if (count <= stripParams.mCountInStrip && !DBG) {
            mCandidatesPaneControl.setVisibility(GONE);
        } else {
            mCandidatesPaneControl.setVisibility(VISIBLE);
            mExpandCandidatesPane.setVisibility(VISIBLE);
            mExpandCandidatesPane.setEnabled(true);
        final int countInStrip = stripParams.mCountInStrip;
        View centeringFrom = null, lastView = null;
        int x = 0, y = 0, infoX = 0;
        for (int index = 0; index < count; index++) {
            final int pos = stripParams.getWordPosition(index);
            final TextView word = mWords.get(pos);
            final View divider = mDividers.get(pos);
            final TextPaint paint = word.getPaint();
            // TODO: Reorder candidates in strip as appropriate. The center candidate should hold
            // the word when space is typed (valid typed word or auto corrected word).
            word.setTextColor(stripParams.getCandidateTextColor(pos));
            final CharSequence styled = stripParams.getWord(pos);
            if (DBG) {
                final CharSequence debugInfo = stripParams.getDebugInfo(index);
                if (debugInfo != null) {
                    info = mInfos.get(index);
                    info.setText(debugInfo);
                } else {
                    info = null;
                }
            final CharSequence text;
            final float scaleX;
            if (index < countInStrip) {
                if (index == 0 && stripParams.mCountInStrip == 1) {
                    text = getEllipsizedText(styled, stripParams.mMaxWidth, paint);
                    scaleX = paint.getTextScaleX();
                } else {
                    text = styled;
                    scaleX = stripParams.mScaleX;
                }
                word.setText(text);
                word.setTextScaleX(scaleX);
                if (index != 0) {
                    // Add divider if this isn't the left most suggestion in candidate strip.
                    mCandidatesStrip.addView(divider);
                }
                mCandidatesStrip.addView(word);
                if (stripParams.mCanUseFixedWidthColumns) {
                    setLayoutWeight(word, 1.0f, mCandidateStripHeight);
                } else {
                    final int width = getTextWidth(text, paint) + stripParams.mPadding;
                    setLayoutWeight(word, width, mCandidateStripHeight);
                if (info != null) {
                    mCandidatesPane.addView(info);
                    info.measure(WRAP_CONTENT, WRAP_CONTENT);
                    final int width = info.getMeasuredWidth();
                    y = info.getMeasuredHeight();
                    FrameLayoutCompatUtils.placeViewAt(info, infoX, 0, width, y);
                    infoX += width * 2;
                paint.setTextScaleX(1.0f);
                final int textWidth = getTextWidth(styled, paint);
                int available = paneWidth - x - paneParams.mPadding;
                if (textWidth >= available) {
                    // Needs new row, centering previous row.
                    centeringCandidates(centeringFrom, lastView, x, paneWidth);
                    x = 0;
                    y += mCandidateStripHeight;
                }
                if (x != 0) {
                    // Add divider if this isn't the left most suggestion in current row.
                    mCandidatesPane.addView(divider);
                    FrameLayoutCompatUtils.placeViewAt(
                            divider, x, y + (mCandidateStripHeight - paneParams.mDividerHeight) / 2,
                            paneParams.mDividerWidth, paneParams.mDividerHeight);
                    x += paneParams.mDividerWidth;
                available = paneWidth - x - paneParams.mPadding;
                text = getEllipsizedText(styled, available, paint);
                scaleX = paint.getTextScaleX();
                word.setText(text);
                word.setTextScaleX(scaleX);
                mCandidatesPane.addView(word);
                lastView = word;
                if (x == 0) centeringFrom = word;
                word.measure(WRAP_CONTENT,
                        MeasureSpec.makeMeasureSpec(mCandidateStripHeight, MeasureSpec.EXACTLY));
                final int width = word.getMeasuredWidth();
                final int height = word.getMeasuredHeight();
                FrameLayoutCompatUtils.placeViewAt(
                        word, x, y + (mCandidateStripHeight - height) / 2, width, height);
                x += width;
                if (info != null) {
                    mCandidatesPane.addView(info);
                    lastView = info;
                    info.measure(WRAP_CONTENT, WRAP_CONTENT);
                    final int infoWidth = info.getMeasuredWidth();
                    FrameLayoutCompatUtils.placeViewAt(
                            info, x - infoWidth, y, infoWidth, info.getMeasuredHeight());
        if (x != 0) {
            // Centering last candidates row.
            centeringCandidates(centeringFrom, lastView, x, paneWidth);
    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 void centeringCandidates(View from, View to, int width, int paneWidth) {
        final ViewGroup pane = mCandidatesPane;
        final int fromIndex = pane.indexOfChild(from);
        final int toIndex = pane.indexOfChild(to);
        final int offset = (paneWidth - width) / 2;
        for (int index = fromIndex; index <= toIndex; index++) {
            offsetMargin(pane.getChildAt(index), offset, 0);
        }
    }

    private static void offsetMargin(View v, int dx, int dy) {
        if (v == null)
            return;
        final ViewGroup.LayoutParams lp = v.getLayoutParams();
        if (lp instanceof ViewGroup.MarginLayoutParams) {
            final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp;
            mlp.setMargins(mlp.leftMargin + dx, mlp.topMargin + dy, 0, 0);
        }
    }

    private static CharSequence getEllipsizedText(CharSequence text, int maxWidth,
            TextPaint paint) {
        paint.setTextScaleX(1.0f);
        final int width = getTextWidth(text, paint);
        final float scaleX = Math.min(maxWidth / (float)width, 1.0f);
        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 ellipsezed 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);
        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;
    private void expandCandidatesPane() {
        mExpandCandidatesPane.setVisibility(GONE);
        mCloseCandidatesPane.setVisibility(VISIBLE);
        mCandidatesPaneContainer.setMinimumHeight(mKeyboardView.getMeasuredHeight());
        mCandidatesPaneContainer.setVisibility(VISIBLE);
        mKeyboardView.setVisibility(GONE);
    private void closeCandidatesPane() {
        mExpandCandidatesPane.setVisibility(VISIBLE);
        mCloseCandidatesPane.setVisibility(GONE);
        mCandidatesPaneContainer.setVisibility(GONE);
        mKeyboardView.setVisibility(VISIBLE);
    public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) {
        final CharSequence inverted = mStripParams.getInvertedText(autoCorrectedWord);
        if (inverted == null)
        final TextView tv = mWords.get(1);
        tv.setText(inverted);
        mShowingAutoCorrectionInverted = true;
    }

    public boolean isShowingAddToDictionaryHint() {
        return mShowingAddToDictionary;
    }

    public void showAddToDictionaryHint(CharSequence word) {
        mWordToSave.setText(word);
        mCandidatesStrip.setVisibility(GONE);
        mCandidatesPaneControl.setVisibility(GONE);
        mTouchToSave.setVisibility(VISIBLE);
    public boolean dismissAddToDictionaryHint() {
        if (!mShowingAddToDictionary) return false;
        clear();
        return true;
    }

    public SuggestedWords getSuggestions() {
        mShowingAutoCorrectionInverted = false;
        mTouchToSave.setVisibility(GONE);
        mCandidatesStrip.setVisibility(VISIBLE);
        mCandidatesStrip.removeAllViews();
        mCandidatesPane.removeAllViews();
        closeCandidatesPane();
        mPreviewPopup.dismiss();

    private void showPreview(int index, CharSequence word) {
        if (TextUtils.isEmpty(word))
            return;

        final TextView previewText = mPreviewText;
        previewText.setTextColor(mStripParams.mColorTypedWord);
        previewText.setText(word);
        previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
        View v = mWords.get(index);
        final int[] offsetInWindow = new int[2];
        v.getLocationInWindow(offsetInWindow);
        final int posX = offsetInWindow[0];
        final int posY = offsetInWindow[1] - previewText.getMeasuredHeight();
        final PopupWindow previewPopup = mPreviewPopup;
        if (previewPopup.isShowing()) {
            previewPopup.update(posX, posY, previewPopup.getWidth(), previewPopup.getHeight());
        } else {
            previewPopup.showAtLocation(this, Gravity.NO_GRAVITY, posX, posY);
        previewText.setVisibility(VISIBLE);
        mHandler.postHidePreview();
    private void addToDictionary(CharSequence word) {
        if (mListener.addWordToDictionary(word.toString())) {
            showPreview(0, getContext().getString(R.string.added_word, word));
        }
    }

    @Override
    public void onClick(View view) {
        if (view == mWordToSave) {
            addToDictionary(((TextView)view).getText());
            clear();
            return;
        }

        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);
        // Because some punctuation letters are not treated as word separator depending on locale,
        // {@link #setSuggestions} might not be called and candidates pane left opened.
        closeCandidatesPane();
    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mHandler.cancelAllMessages();