SuggestionStripView.java revision 74b6897a12ec603ef835aaa77a01f0c32f49aa1c
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.latin;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.graphics.Color;
23import android.graphics.Typeface;
24import android.os.Message;
25import android.text.Spannable;
26import android.text.SpannableString;
27import android.text.Spanned;
28import android.text.TextPaint;
29import android.text.TextUtils;
30import android.text.style.BackgroundColorSpan;
31import android.text.style.CharacterStyle;
32import android.text.style.ForegroundColorSpan;
33import android.text.style.StyleSpan;
34import android.text.style.UnderlineSpan;
35import android.util.AttributeSet;
36import android.view.Gravity;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.View.OnClickListener;
40import android.view.View.OnLongClickListener;
41import android.view.ViewGroup;
42import android.widget.LinearLayout;
43import android.widget.PopupWindow;
44import android.widget.TextView;
45
46import com.android.inputmethod.compat.FrameLayoutCompatUtils;
47import com.android.inputmethod.compat.LinearLayoutCompatUtils;
48import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
49
50import java.util.ArrayList;
51import java.util.List;
52
53public class CandidateView extends LinearLayout implements OnClickListener, OnLongClickListener {
54
55    public interface Listener {
56        public boolean addWordToDictionary(String word);
57        public void pickSuggestionManually(int index, CharSequence word);
58    }
59
60    private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
61    private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
62    // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
63    private static final int MAX_SUGGESTIONS = 18;
64    private static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
65    private static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
66
67    private static final boolean DBG = LatinImeLogger.sDBG;
68
69    private final ViewGroup mCandidatesStrip;
70    private final int mCandidateCountInStrip;
71    private static final int DEFAULT_CANDIDATE_COUNT_IN_STRIP = 3;
72    private final ViewGroup mCandidatesPaneControl;
73    private final TextView mExpandCandidatesPane;
74    private final TextView mCloseCandidatesPane;
75    private ViewGroup mCandidatesPane;
76    private ViewGroup mCandidatesPaneContainer;
77    private View mKeyboardView;
78
79    private final ArrayList<TextView> mWords = new ArrayList<TextView>();
80    private final ArrayList<TextView> mInfos = new ArrayList<TextView>();
81    private final ArrayList<View> mDividers = new ArrayList<View>();
82
83    private final int mCandidateStripHeight;
84    private final CharacterStyle mInvertedForegroundColorSpan;
85    private final CharacterStyle mInvertedBackgroundColorSpan;
86    private final int mAutoCorrectHighlight;
87    private static final int AUTO_CORRECT_BOLD = 0x01;
88    private static final int AUTO_CORRECT_UNDERLINE = 0x02;
89    private static final int AUTO_CORRECT_INVERT = 0x04;
90    private final int mColorTypedWord;
91    private final int mColorAutoCorrect;
92    private final int mColorSuggestedCandidate;
93    private final int mColorDivider;
94
95    private final PopupWindow mPreviewPopup;
96    private final TextView mPreviewText;
97
98    private final View mTouchToSave;
99    private final TextView mWordToSave;
100
101    private Listener mListener;
102    private SuggestedWords mSuggestions = SuggestedWords.EMPTY;
103    private boolean mShowingAutoCorrectionInverted;
104    private boolean mShowingAddToDictionary;
105
106    private final CandidateViewLayoutParams mParams;
107    private static final int PUNCTUATIONS_IN_STRIP = 6;
108    private static final float MIN_TEXT_XSCALE = 0.8f;
109
110    private final UiHandler mHandler = new UiHandler(this);
111
112    private static class UiHandler extends StaticInnerHandlerWrapper<CandidateView> {
113        private static final int MSG_HIDE_PREVIEW = 0;
114        private static final int MSG_UPDATE_SUGGESTION = 1;
115
116        private static final long DELAY_HIDE_PREVIEW = 1000;
117        private static final long DELAY_UPDATE_SUGGESTION = 300;
118
119        public UiHandler(CandidateView outerInstance) {
120            super(outerInstance);
121        }
122
123        @Override
124        public void dispatchMessage(Message msg) {
125            final CandidateView candidateView = getOuterInstance();
126            switch (msg.what) {
127            case MSG_HIDE_PREVIEW:
128                candidateView.hidePreview();
129                break;
130            case MSG_UPDATE_SUGGESTION:
131                candidateView.updateSuggestions();
132                break;
133            }
134        }
135
136        public void postHidePreview() {
137            cancelHidePreview();
138            sendMessageDelayed(obtainMessage(MSG_HIDE_PREVIEW), DELAY_HIDE_PREVIEW);
139        }
140
141        public void cancelHidePreview() {
142            removeMessages(MSG_HIDE_PREVIEW);
143        }
144
145        public void postUpdateSuggestions() {
146            cancelUpdateSuggestions();
147            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION),
148                    DELAY_UPDATE_SUGGESTION);
149        }
150
151        public void cancelUpdateSuggestions() {
152            removeMessages(MSG_UPDATE_SUGGESTION);
153        }
154
155        public void cancelAllMessages() {
156            cancelHidePreview();
157            cancelUpdateSuggestions();
158        }
159    }
160
161    private static class CandidateViewLayoutParams {
162        public final TextPaint mPaint;
163        public final int mPadding;
164        public final int mDividerWidth;
165        public final int mDividerHeight;
166        public final int mControlWidth;
167        private final int mAutoCorrectHighlight;
168
169        public final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
170
171        public int mCountInStrip;
172        // True if the mCountInStrip suggestions can fit in suggestion strip in equally divided
173        // width without squeezing the text.
174        public boolean mCanUseFixedWidthColumns;
175        public int mMaxWidth;
176        public int mAvailableWidthForWords;
177        public int mConstantWidthForPaddings;
178        public int mVariableWidthForWords;
179        public float mScaleX;
180
181        public CandidateViewLayoutParams(Resources res, View divider, View control,
182                int autoCorrectHighlight) {
183            mPaint = new TextPaint();
184            final float textSize = res.getDimension(R.dimen.candidate_text_size);
185            mPaint.setTextSize(textSize);
186            mPadding = res.getDimensionPixelSize(R.dimen.candidate_padding);
187            mDividerWidth = divider.getMeasuredWidth();
188            mDividerHeight = divider.getMeasuredHeight();
189            mControlWidth = control.getMeasuredWidth();
190            mAutoCorrectHighlight = autoCorrectHighlight;
191        }
192
193        public void layoutStrip(SuggestedWords suggestions, int maxWidth, int maxCount) {
194            final int size = suggestions.size();
195            setupTexts(suggestions, size, mAutoCorrectHighlight);
196            mCountInStrip = Math.min(maxCount, size);
197            mScaleX = 1.0f;
198
199            do {
200                mMaxWidth = maxWidth;
201                if (size > mCountInStrip) {
202                    mMaxWidth -= mControlWidth;
203                }
204
205                tryLayout();
206
207                if (mCanUseFixedWidthColumns) {
208                    return;
209                }
210                if (mVariableWidthForWords <= mAvailableWidthForWords) {
211                    return;
212                }
213
214                final float scaleX = mAvailableWidthForWords / (float)mVariableWidthForWords;
215                if (scaleX >= MIN_TEXT_XSCALE) {
216                    mScaleX = scaleX;
217                    return;
218                }
219
220                mCountInStrip--;
221            } while (mCountInStrip > 1);
222        }
223
224        public void tryLayout() {
225            final int maxCount = mCountInStrip;
226            final int dividers = mDividerWidth * (maxCount - 1);
227            mConstantWidthForPaddings = dividers + mPadding * maxCount * 2;
228            mAvailableWidthForWords = mMaxWidth - mConstantWidthForPaddings;
229
230            mPaint.setTextScaleX(mScaleX);
231            final int maxFixedWidthForWord = (mMaxWidth - dividers) / maxCount - mPadding * 2;
232            mCanUseFixedWidthColumns = true;
233            mVariableWidthForWords = 0;
234            for (int i = 0; i < maxCount; i++) {
235                final int width = getTextWidth(mTexts.get(i), mPaint);
236                if (width > maxFixedWidthForWord)
237                    mCanUseFixedWidthColumns = false;
238                mVariableWidthForWords += width;
239            }
240        }
241
242        private void setupTexts(SuggestedWords suggestions, int count, int autoCorrectHighlight) {
243            mTexts.clear();
244            for (int i = 0; i < count; i++) {
245                final CharSequence suggestion = suggestions.getWord(i);
246                if (suggestion == null) continue;
247
248                final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion
249                        && ((i == 1 && !suggestions.mTypedWordValid)
250                                || (i == 0 && suggestions.mTypedWordValid));
251                // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1
252                // and there are multiple suggestions, such as the default punctuation list.
253                // TODO: Need to revisit this logic with bigram suggestions
254                final CharSequence styled = getStyledCandidateWord(suggestion, isAutoCorrect,
255                        autoCorrectHighlight);
256                mTexts.add(styled);
257            }
258        }
259
260        @Override
261        public String toString() {
262            return String.format(
263                    "count=%d width=%d avail=%d fixcol=%s scaleX=%4.2f const=%d var=%d",
264                    mCountInStrip, mMaxWidth, mAvailableWidthForWords, mCanUseFixedWidthColumns,
265                    mScaleX, mConstantWidthForPaddings, mVariableWidthForWords);
266        }
267    }
268
269    /**
270     * Construct a CandidateView for showing suggested words for completion.
271     * @param context
272     * @param attrs
273     */
274    public CandidateView(Context context, AttributeSet attrs) {
275        this(context, attrs, R.attr.candidateViewStyle);
276    }
277
278    public CandidateView(Context context, AttributeSet attrs, int defStyle) {
279        // Note: Up to version 10 (Gingerbread) of the API, LinearLayout doesn't have 3-argument
280        // constructor.
281        // TODO: Call 3-argument constructor, super(context, attrs, defStyle), when we abandon
282        // backward compatibility with the version 10 or earlier of the API.
283        super(context, attrs);
284        if (defStyle != R.attr.candidateViewStyle) {
285            throw new IllegalArgumentException(
286                    "can't accept defStyle other than R.attr.candidayeViewStyle: defStyle="
287                    + defStyle);
288        }
289        setBackgroundDrawable(LinearLayoutCompatUtils.getBackgroundDrawable(
290                context, attrs, defStyle, R.style.CandidateViewStyle));
291
292        final TypedArray a = context.obtainStyledAttributes(
293                attrs, R.styleable.CandidateView, defStyle, R.style.CandidateViewStyle);
294        mAutoCorrectHighlight = a.getInt(R.styleable.CandidateView_autoCorrectHighlight, 0);
295        mColorTypedWord = a.getColor(R.styleable.CandidateView_colorTypedWord, 0);
296        mColorAutoCorrect = a.getColor(R.styleable.CandidateView_colorAutoCorrect, 0);
297        mColorSuggestedCandidate = a.getColor(R.styleable.CandidateView_colorSuggested, 0);
298        mColorDivider = a.getColor(R.styleable.CandidateView_colorDivider, 0);
299        mCandidateCountInStrip = a.getInt(
300                R.styleable.CandidateView_candidateCountInStrip, DEFAULT_CANDIDATE_COUNT_IN_STRIP);
301        a.recycle();
302
303        Resources res = context.getResources();
304        LayoutInflater inflater = LayoutInflater.from(context);
305        inflater.inflate(R.layout.candidates_strip, this);
306
307        mPreviewPopup = new PopupWindow(context);
308        mPreviewText = (TextView) inflater.inflate(R.layout.candidate_preview, null);
309        mPreviewPopup.setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT,
310                ViewGroup.LayoutParams.WRAP_CONTENT);
311        mPreviewPopup.setContentView(mPreviewText);
312        mPreviewPopup.setBackgroundDrawable(null);
313
314        mCandidatesStrip = (ViewGroup)findViewById(R.id.candidates_strip);
315        mCandidateStripHeight = res.getDimensionPixelOffset(R.dimen.candidate_strip_height);
316        for (int i = 0; i < MAX_SUGGESTIONS; i++) {
317            final TextView word = (TextView)inflater.inflate(R.layout.candidate_word, null);
318            word.setTag(i);
319            word.setOnClickListener(this);
320            if (i == 0)
321                word.setOnLongClickListener(this);
322            mWords.add(word);
323            mInfos.add((TextView)inflater.inflate(R.layout.candidate_info, null));
324            mDividers.add(getDivider(inflater));
325        }
326
327        mTouchToSave = findViewById(R.id.touch_to_save);
328        mWordToSave = (TextView)findViewById(R.id.word_to_save);
329        mWordToSave.setOnClickListener(this);
330
331        mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorTypedWord ^ 0x00ffffff);
332        mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorTypedWord);
333
334        mCandidatesPaneControl = (ViewGroup)findViewById(R.id.candidates_pane_control);
335        mExpandCandidatesPane = (TextView)findViewById(R.id.expand_candidates_pane);
336        mExpandCandidatesPane.setOnClickListener(new OnClickListener() {
337            @Override
338            public void onClick(View view) {
339                expandCandidatesPane();
340            }
341        });
342        mCloseCandidatesPane = (TextView)findViewById(R.id.close_candidates_pane);
343        mCloseCandidatesPane.setOnClickListener(new OnClickListener() {
344            @Override
345            public void onClick(View view) {
346                closeCandidatesPane();
347            }
348        });
349        mCandidatesPaneControl.measure(WRAP_CONTENT, WRAP_CONTENT);
350
351        mParams = new CandidateViewLayoutParams(res, mDividers.get(0), mCandidatesPaneControl,
352                mAutoCorrectHighlight);
353    }
354
355    private View getDivider(LayoutInflater inflater) {
356        final TextView divider = (TextView)inflater.inflate(R.layout.candidate_divider, null);
357        divider.setTextColor(mColorDivider);
358        divider.measure(WRAP_CONTENT, WRAP_CONTENT);
359        return divider;
360    }
361
362    /**
363     * A connection back to the input method.
364     * @param listener
365     */
366    public void setListener(Listener listener, View inputView) {
367        mListener = listener;
368        mKeyboardView = inputView.findViewById(R.id.keyboard_view);
369        mCandidatesPane = FrameLayoutCompatUtils.getPlacer(
370                (ViewGroup)inputView.findViewById(R.id.candidates_pane));
371        mCandidatesPane.setOnClickListener(this);
372        mCandidatesPaneContainer = (ViewGroup)inputView.findViewById(
373                R.id.candidates_pane_container);
374    }
375
376    public void setSuggestions(SuggestedWords suggestions) {
377        if (suggestions == null)
378            return;
379        mSuggestions = suggestions;
380        if (mShowingAutoCorrectionInverted) {
381            mHandler.postUpdateSuggestions();
382        } else {
383            updateSuggestions();
384        }
385    }
386
387    private static CharSequence getStyledCandidateWord(CharSequence word, boolean isAutoCorrect,
388            int autoCorrectHighlight) {
389        if (!isAutoCorrect)
390            return word;
391        final Spannable spannedWord = new SpannableString(word);
392        if ((autoCorrectHighlight & AUTO_CORRECT_BOLD) != 0)
393            spannedWord.setSpan(BOLD_SPAN, 0, word.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
394        if ((autoCorrectHighlight & AUTO_CORRECT_UNDERLINE) != 0)
395            spannedWord.setSpan(UNDERLINE_SPAN, 0, word.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
396        return spannedWord;
397    }
398
399    private int getCandidateTextColor(boolean isAutoCorrect, boolean isSuggestedCandidate,
400            SuggestedWordInfo info) {
401        final int color;
402        if (isAutoCorrect) {
403            color = mColorAutoCorrect;
404        } else if (isSuggestedCandidate) {
405            color = mColorSuggestedCandidate;
406        } else {
407            color = mColorTypedWord;
408        }
409        if (info != null && info.isPreviousSuggestedWord()) {
410            final int newAlpha = (int)(Color.alpha(color) * 0.5f);
411            return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
412        } else {
413            return color;
414        }
415    }
416
417    private void updateSuggestions() {
418        final SuggestedWords suggestions = mSuggestions;
419        final List<SuggestedWordInfo> suggestedWordInfoList = suggestions.mSuggestedWordInfoList;
420        final int paneWidth = getWidth();
421        final CandidateViewLayoutParams params = mParams;
422
423        clear();
424        closeCandidatesPane();
425        if (suggestions.size() == 0)
426            return;
427
428        params.layoutStrip(suggestions, paneWidth, suggestions.isPunctuationSuggestions()
429                ? PUNCTUATIONS_IN_STRIP : mCandidateCountInStrip);
430
431        final int count = Math.min(mWords.size(), suggestions.size());
432        if (count <= params.mCountInStrip) {
433            mCandidatesPaneControl.setVisibility(GONE);
434        } else {
435            mCandidatesPaneControl.setVisibility(VISIBLE);
436            mExpandCandidatesPane.setVisibility(VISIBLE);
437        }
438
439        final int countInStrip = params.mCountInStrip;
440        int fromIndex = countInStrip;
441        int x = 0, y = 0;
442        for (int i = 0; i < count; i++) {
443            final int pos;
444            if (i <= 1) {
445                final boolean willAutoCorrect = !suggestions.mTypedWordValid
446                        && suggestions.mHasMinimalSuggestion;
447                pos = willAutoCorrect ? 1 - i : i;
448            } else {
449                pos = i;
450            }
451            final CharSequence suggestion = suggestions.getWord(pos);
452            if (suggestion == null) continue;
453
454            final SuggestedWordInfo suggestionInfo = (suggestedWordInfoList != null)
455                    ? suggestedWordInfoList.get(pos) : null;
456            final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion
457                    && ((pos == 1 && !suggestions.mTypedWordValid)
458                            || (pos == 0 && suggestions.mTypedWordValid));
459            // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1
460            // and there are multiple suggestions, such as the default punctuation list.
461            // TODO: Need to revisit this logic with bigram suggestions
462            final boolean isSuggestedCandidate = (pos != 0);
463            final boolean isPunctuationSuggestions = (suggestion.length() == 1 && count > 1);
464
465            final TextView word = mWords.get(pos);
466            final TextPaint paint = word.getPaint();
467            // TODO: Reorder candidates in strip as appropriate. The center candidate should hold
468            // the word when space is typed (valid typed word or auto corrected word).
469            word.setTextColor(getCandidateTextColor(isAutoCorrect,
470                    isSuggestedCandidate || isPunctuationSuggestions, suggestionInfo));
471            final CharSequence styled = params.mTexts.get(pos);
472
473            final TextView info;
474            if (DBG && suggestionInfo != null
475                    && !TextUtils.isEmpty(suggestionInfo.getDebugString())) {
476                info = mInfos.get(i);
477                info.setText(suggestionInfo.getDebugString());
478            } else {
479                info = null;
480            }
481
482            final CharSequence text;
483            final float scaleX;
484            if (i < countInStrip) {
485                if (i == 0 && params.mCountInStrip == 1) {
486                    text = getEllipsizedText(styled, params.mMaxWidth, paint);
487                    scaleX = paint.getTextScaleX();
488                } else {
489                    text = styled;
490                    scaleX = params.mScaleX;
491                }
492                word.setText(text);
493                word.setTextScaleX(scaleX);
494                if (i != 0) {
495                    // Add divider if this isn't the left most suggestion in candidate strip.
496                    mCandidatesStrip.addView(mDividers.get(i));
497                }
498                mCandidatesStrip.addView(word);
499                if (params.mCanUseFixedWidthColumns) {
500                    setLayoutWeight(word, 1.0f);
501                } else {
502                    final int width = getTextWidth(text, paint) + params.mPadding * 2;
503                    setLayoutWeight(word, width);
504                }
505                if (info != null) {
506                    word.measure(WRAP_CONTENT, MATCH_PARENT);
507                    final int width = word.getMeasuredWidth();
508                    info.measure(WRAP_CONTENT, WRAP_CONTENT);
509                    final int infoWidth = info.getMeasuredWidth();
510                    FrameLayoutCompatUtils.placeViewAt(
511                            info, width - infoWidth, 0, infoWidth, info.getMeasuredHeight());
512                }
513            } else {
514                paint.setTextScaleX(1.0f);
515                final int textWidth = getTextWidth(styled, paint);
516                int available = paneWidth - x - params.mPadding * 2;
517                if (textWidth >= available) {
518                    // Needs new row, centering previous row.
519                    centeringCandidates(fromIndex, i - 1, x, paneWidth);
520                    x = 0;
521                    y += mCandidateStripHeight;
522                    fromIndex = i;
523                }
524                if (x != 0) {
525                    // Add divider if this isn't the left most suggestion in current row.
526                    final View divider = mDividers.get(i);
527                    mCandidatesPane.addView(divider);
528                    FrameLayoutCompatUtils.placeViewAt(
529                            divider, x, y + (mCandidateStripHeight - params.mDividerHeight) / 2,
530                            params.mDividerWidth, params.mDividerHeight);
531                    x += params.mDividerWidth;
532                }
533                available = paneWidth - x - params.mPadding * 2;
534                text = getEllipsizedText(styled, available, paint);
535                scaleX = paint.getTextScaleX();
536                word.setText(text);
537                word.setTextScaleX(scaleX);
538                mCandidatesPane.addView(word);
539                word.measure(WRAP_CONTENT, WRAP_CONTENT);
540                final int width = word.getMeasuredWidth();
541                final int height = word.getMeasuredHeight();
542                FrameLayoutCompatUtils.placeViewAt(
543                        word, x, y + (mCandidateStripHeight - height) / 2, width, height);
544                if (info != null) {
545                    mCandidatesPane.addView(info);
546                    info.measure(WRAP_CONTENT, WRAP_CONTENT);
547                    final int infoWidth = info.getMeasuredWidth();
548                    FrameLayoutCompatUtils.placeViewAt(
549                            info, x + width - infoWidth, y, infoWidth, info.getMeasuredHeight());
550                }
551                x += width;
552            }
553        }
554        if (x != 0) {
555            // Centering last candidates row.
556            centeringCandidates(fromIndex, count - 1, x, paneWidth);
557        }
558    }
559
560    private static void setLayoutWeight(View v, float weight) {
561        final ViewGroup.LayoutParams lp = v.getLayoutParams();
562        if (lp instanceof LinearLayout.LayoutParams) {
563            final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
564            llp.weight = weight;
565            llp.width = 0;
566            llp.height = MATCH_PARENT;
567        }
568    }
569
570    private void centeringCandidates(int from, int to, int width, int paneWidth) {
571        final ViewGroup pane = mCandidatesPane;
572        final int fromIndex = pane.indexOfChild(mWords.get(from));
573        final int toIndex;
574        if (mInfos.get(to).getParent() != null) {
575            toIndex = pane.indexOfChild(mInfos.get(to));
576        } else {
577            toIndex = pane.indexOfChild(mWords.get(to));
578        }
579        final int offset = (paneWidth - width) / 2;
580        for (int index = fromIndex; index <= toIndex; index++) {
581            offsetMargin(pane.getChildAt(index), offset, 0);
582        }
583    }
584
585    private static void offsetMargin(View v, int dx, int dy) {
586        if (v == null)
587            return;
588        final ViewGroup.LayoutParams lp = v.getLayoutParams();
589        if (lp instanceof ViewGroup.MarginLayoutParams) {
590            final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp;
591            mlp.setMargins(mlp.leftMargin + dx, mlp.topMargin + dy, 0, 0);
592        }
593    }
594
595    private static CharSequence getEllipsizedText(CharSequence text, int maxWidth,
596            TextPaint paint) {
597        paint.setTextScaleX(1.0f);
598        final int width = getTextWidth(text, paint);
599        final float scaleX = Math.min(maxWidth / (float)width, 1.0f);
600        if (scaleX >= MIN_TEXT_XSCALE) {
601            paint.setTextScaleX(scaleX);
602            return text;
603        }
604
605        // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To get
606        // squeezed and ellipsezed text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
607        final CharSequence ellipsized = TextUtils.ellipsize(
608                text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE);
609        paint.setTextScaleX(MIN_TEXT_XSCALE);
610        return ellipsized;
611    }
612
613    private static int getTextWidth(CharSequence text, TextPaint paint) {
614        if (TextUtils.isEmpty(text)) return 0;
615        paint.setTypeface(getTextTypeface(text));
616        final int len = text.length();
617        final float[] widths = new float[len];
618        final int count = paint.getTextWidths(text, 0, len, widths);
619        float width = 0;
620        for (int i = 0; i < count; i++) {
621            width += widths[i];
622        }
623        return (int)Math.round(width + 0.5);
624    }
625
626    private static Typeface getTextTypeface(CharSequence text) {
627        if (!(text instanceof SpannableString))
628            return Typeface.DEFAULT;
629
630        final SpannableString ss = (SpannableString)text;
631        final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class);
632        if (styles.length == 0)
633            return Typeface.DEFAULT;
634
635        switch (styles[0].getStyle()) {
636        case Typeface.BOLD: return Typeface.DEFAULT_BOLD;
637        // TODO: BOLD_ITALIC, ITALIC case?
638        default: return Typeface.DEFAULT;
639        }
640    }
641
642    private void expandCandidatesPane() {
643        mExpandCandidatesPane.setVisibility(GONE);
644        mCloseCandidatesPane.setVisibility(VISIBLE);
645        mCandidatesPaneContainer.setMinimumHeight(mKeyboardView.getMeasuredHeight());
646        mCandidatesPaneContainer.setVisibility(VISIBLE);
647        mKeyboardView.setVisibility(GONE);
648    }
649
650    private void closeCandidatesPane() {
651        mExpandCandidatesPane.setVisibility(VISIBLE);
652        mCloseCandidatesPane.setVisibility(GONE);
653        mCandidatesPaneContainer.setVisibility(GONE);
654        mKeyboardView.setVisibility(VISIBLE);
655    }
656
657    public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) {
658        if ((mAutoCorrectHighlight & AUTO_CORRECT_INVERT) == 0)
659            return;
660        final TextView tv = mWords.get(1);
661        final Spannable word = new SpannableString(autoCorrectedWord);
662        final int wordLength = word.length();
663        word.setSpan(mInvertedBackgroundColorSpan, 0, wordLength,
664                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
665        word.setSpan(mInvertedForegroundColorSpan, 0, wordLength,
666                Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
667        tv.setText(word);
668        mShowingAutoCorrectionInverted = true;
669    }
670
671    public boolean isShowingAddToDictionaryHint() {
672        return mShowingAddToDictionary;
673    }
674
675    public void showAddToDictionaryHint(CharSequence word) {
676        mWordToSave.setText(word);
677        mShowingAddToDictionary = true;
678        mCandidatesStrip.setVisibility(GONE);
679        mTouchToSave.setVisibility(VISIBLE);
680    }
681
682    public boolean dismissAddToDictionaryHint() {
683        if (!mShowingAddToDictionary) return false;
684        clear();
685        return true;
686    }
687
688    public SuggestedWords getSuggestions() {
689        return mSuggestions;
690    }
691
692    public void clear() {
693        mShowingAddToDictionary = false;
694        mShowingAutoCorrectionInverted = false;
695        mTouchToSave.setVisibility(GONE);
696        mCandidatesStrip.setVisibility(VISIBLE);
697        mCandidatesStrip.removeAllViews();
698        mCandidatesPane.removeAllViews();
699    }
700
701    private void hidePreview() {
702        mPreviewPopup.dismiss();
703    }
704
705    private void showPreview(int index, CharSequence word) {
706        if (TextUtils.isEmpty(word))
707            return;
708
709        final TextView previewText = mPreviewText;
710        previewText.setTextColor(mColorTypedWord);
711        previewText.setText(word);
712        previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
713                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
714        View v = mWords.get(index);
715        final int[] offsetInWindow = new int[2];
716        v.getLocationInWindow(offsetInWindow);
717        final int posX = offsetInWindow[0];
718        final int posY = offsetInWindow[1] - previewText.getMeasuredHeight();
719        final PopupWindow previewPopup = mPreviewPopup;
720        if (previewPopup.isShowing()) {
721            previewPopup.update(posX, posY, previewPopup.getWidth(), previewPopup.getHeight());
722        } else {
723            previewPopup.showAtLocation(this, Gravity.NO_GRAVITY, posX, posY);
724        }
725        previewText.setVisibility(VISIBLE);
726        mHandler.postHidePreview();
727    }
728
729    private void addToDictionary(CharSequence word) {
730        if (mListener.addWordToDictionary(word.toString())) {
731            showPreview(0, getContext().getString(R.string.added_word, word));
732        }
733    }
734
735    @Override
736    public boolean onLongClick(View view) {
737        final Object tag = view.getTag();
738        if (!(tag instanceof Integer))
739            return true;
740        final int index = (Integer) tag;
741        if (index >= mSuggestions.size())
742            return true;
743
744        final CharSequence word = mSuggestions.getWord(index);
745        if (word.length() < 2)
746            return false;
747        addToDictionary(word);
748        return true;
749    }
750
751    @Override
752    public void onClick(View view) {
753        if (view == mWordToSave) {
754            addToDictionary(((TextView)view).getText());
755            clear();
756            return;
757        }
758
759        final Object tag = view.getTag();
760        if (!(tag instanceof Integer))
761            return;
762        final int index = (Integer) tag;
763        if (index >= mSuggestions.size())
764            return;
765
766        final CharSequence word = mSuggestions.getWord(index);
767        mListener.pickSuggestionManually(index, word);
768        // Because some punctuation letters are not treated as word separator depending on locale,
769        // {@link #setSuggestions} might not be called and candidates pane left opened.
770        closeCandidatesPane();
771    }
772
773    @Override
774    public void onDetachedFromWindow() {
775        super.onDetachedFromWindow();
776        mHandler.cancelAllMessages();
777        hidePreview();
778    }
779}
780