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