Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2011 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 
     17 package com.android.inputmethod.latin;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Bitmap;
     23 import android.graphics.Canvas;
     24 import android.graphics.Color;
     25 import android.graphics.Paint;
     26 import android.graphics.Paint.Align;
     27 import android.graphics.Rect;
     28 import android.graphics.Typeface;
     29 import android.graphics.drawable.BitmapDrawable;
     30 import android.graphics.drawable.ColorDrawable;
     31 import android.graphics.drawable.Drawable;
     32 import android.os.Message;
     33 import android.os.SystemClock;
     34 import android.text.Spannable;
     35 import android.text.SpannableString;
     36 import android.text.Spanned;
     37 import android.text.TextPaint;
     38 import android.text.TextUtils;
     39 import android.text.style.CharacterStyle;
     40 import android.text.style.StyleSpan;
     41 import android.text.style.UnderlineSpan;
     42 import android.util.AttributeSet;
     43 import android.view.GestureDetector;
     44 import android.view.Gravity;
     45 import android.view.LayoutInflater;
     46 import android.view.MotionEvent;
     47 import android.view.View;
     48 import android.view.View.OnClickListener;
     49 import android.view.View.OnLongClickListener;
     50 import android.view.ViewGroup;
     51 import android.widget.LinearLayout;
     52 import android.widget.PopupWindow;
     53 import android.widget.RelativeLayout;
     54 import android.widget.TextView;
     55 
     56 import com.android.inputmethod.compat.FrameLayoutCompatUtils;
     57 import com.android.inputmethod.keyboard.KeyboardActionListener;
     58 import com.android.inputmethod.keyboard.KeyboardView;
     59 import com.android.inputmethod.keyboard.MoreKeysPanel;
     60 import com.android.inputmethod.keyboard.PointerTracker;
     61 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
     62 
     63 import java.util.ArrayList;
     64 import java.util.List;
     65 
     66 public class SuggestionsView extends RelativeLayout implements OnClickListener,
     67         OnLongClickListener {
     68     public interface Listener {
     69         public boolean addWordToDictionary(String word);
     70         public void pickSuggestionManually(int index, CharSequence word);
     71     }
     72 
     73     // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}.
     74     public static final int MAX_SUGGESTIONS = 18;
     75 
     76     private static final boolean DBG = LatinImeLogger.sDBG;
     77 
     78     private final ViewGroup mSuggestionsStrip;
     79     private KeyboardView mKeyboardView;
     80 
     81     private final View mMoreSuggestionsContainer;
     82     private final MoreSuggestionsView mMoreSuggestionsView;
     83     private final MoreSuggestions.Builder mMoreSuggestionsBuilder;
     84     private final PopupWindow mMoreSuggestionsWindow;
     85 
     86     private final ArrayList<TextView> mWords = new ArrayList<TextView>();
     87     private final ArrayList<TextView> mInfos = new ArrayList<TextView>();
     88     private final ArrayList<View> mDividers = new ArrayList<View>();
     89 
     90     private final PopupWindow mPreviewPopup;
     91     private final TextView mPreviewText;
     92 
     93     private Listener mListener;
     94     private SuggestedWords mSuggestions = SuggestedWords.EMPTY;
     95 
     96     private final SuggestionsViewParams mParams;
     97     private static final float MIN_TEXT_XSCALE = 0.70f;
     98 
     99     private final UiHandler mHandler = new UiHandler(this);
    100 
    101     private static class UiHandler extends StaticInnerHandlerWrapper<SuggestionsView> {
    102         private static final int MSG_HIDE_PREVIEW = 0;
    103 
    104         private static final long DELAY_HIDE_PREVIEW = 1300;
    105 
    106         public UiHandler(SuggestionsView outerInstance) {
    107             super(outerInstance);
    108         }
    109 
    110         @Override
    111         public void dispatchMessage(Message msg) {
    112             final SuggestionsView suggestionsView = getOuterInstance();
    113             switch (msg.what) {
    114             case MSG_HIDE_PREVIEW:
    115                 suggestionsView.hidePreview();
    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 cancelAllMessages() {
    130             cancelHidePreview();
    131         }
    132     }
    133 
    134     private static class SuggestionsViewParams {
    135         private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3;
    136         private static final int DEFAULT_CENTER_SUGGESTION_PERCENTILE = 40;
    137         private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2;
    138         private static final int PUNCTUATIONS_IN_STRIP = 5;
    139 
    140         public final int mPadding;
    141         public final int mDividerWidth;
    142         public final int mSuggestionsStripHeight;
    143         public final int mSuggestionsCountInStrip;
    144         public final int mMaxMoreSuggestionsRow;
    145         public final float mMinMoreSuggestionsWidth;
    146         public final int mMoreSuggestionsBottomGap;
    147 
    148         private final List<TextView> mWords;
    149         private final List<View> mDividers;
    150         private final List<TextView> mInfos;
    151 
    152         private final int mColorTypedWord;
    153         private final int mColorAutoCorrect;
    154         private final int mColorSuggested;
    155         private final float mAlphaObsoleted;
    156         private final float mCenterSuggestionWeight;
    157         private final int mCenterSuggestionIndex;
    158         private final Drawable mMoreSuggestionsHint;
    159         private static final String MORE_SUGGESTIONS_HINT = "\u2026";
    160 
    161         private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD);
    162         private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan();
    163         private static final int AUTO_CORRECT_BOLD = 0x01;
    164         private static final int AUTO_CORRECT_UNDERLINE = 0x02;
    165         private static final int VALID_TYPED_WORD_BOLD = 0x04;
    166 
    167         private final int mSuggestionStripOption;
    168 
    169         private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>();
    170 
    171         public boolean mMoreSuggestionsAvailable;
    172 
    173         public final TextView mWordToSaveView;
    174         private final TextView mHintToSaveView;
    175         private final CharSequence mHintToSaveText;
    176 
    177         public SuggestionsViewParams(Context context, AttributeSet attrs, int defStyle,
    178                 List<TextView> words, List<View> dividers, List<TextView> infos) {
    179             mWords = words;
    180             mDividers = dividers;
    181             mInfos = infos;
    182 
    183             final TextView word = words.get(0);
    184             final View divider = dividers.get(0);
    185             mPadding = word.getCompoundPaddingLeft() + word.getCompoundPaddingRight();
    186             divider.measure(
    187                     ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    188             mDividerWidth = divider.getMeasuredWidth();
    189 
    190             final Resources res = word.getResources();
    191             mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height);
    192 
    193             final TypedArray a = context.obtainStyledAttributes(
    194                     attrs, R.styleable.SuggestionsView, defStyle, R.style.SuggestionsViewStyle);
    195             mSuggestionStripOption = a.getInt(R.styleable.SuggestionsView_suggestionStripOption, 0);
    196             final float alphaTypedWord = getPercent(a,
    197                     R.styleable.SuggestionsView_alphaTypedWord, 100);
    198             final float alphaAutoCorrect = getPercent(a,
    199                     R.styleable.SuggestionsView_alphaAutoCorrect, 100);
    200             final float alphaSuggested = getPercent(a,
    201                     R.styleable.SuggestionsView_alphaSuggested, 100);
    202             mAlphaObsoleted = getPercent(a, R.styleable.SuggestionsView_alphaSuggested, 100);
    203             mColorTypedWord = applyAlpha(
    204                     a.getColor(R.styleable.SuggestionsView_colorTypedWord, 0), alphaTypedWord);
    205             mColorAutoCorrect = applyAlpha(
    206                     a.getColor(R.styleable.SuggestionsView_colorAutoCorrect, 0), alphaAutoCorrect);
    207             mColorSuggested = applyAlpha(
    208                     a.getColor(R.styleable.SuggestionsView_colorSuggested, 0), alphaSuggested);
    209             mSuggestionsCountInStrip = a.getInt(
    210                     R.styleable.SuggestionsView_suggestionsCountInStrip,
    211                     DEFAULT_SUGGESTIONS_COUNT_IN_STRIP);
    212             mCenterSuggestionWeight = getPercent(a,
    213                     R.styleable.SuggestionsView_centerSuggestionPercentile,
    214                     DEFAULT_CENTER_SUGGESTION_PERCENTILE);
    215             mMaxMoreSuggestionsRow = a.getInt(
    216                     R.styleable.SuggestionsView_maxMoreSuggestionsRow,
    217                     DEFAULT_MAX_MORE_SUGGESTIONS_ROW);
    218             mMinMoreSuggestionsWidth = getRatio(a,
    219                     R.styleable.SuggestionsView_minMoreSuggestionsWidth);
    220             a.recycle();
    221 
    222             mMoreSuggestionsHint = getMoreSuggestionsHint(res,
    223                     res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect);
    224             mCenterSuggestionIndex = mSuggestionsCountInStrip / 2;
    225             mMoreSuggestionsBottomGap = res.getDimensionPixelOffset(
    226                     R.dimen.more_suggestions_bottom_gap);
    227 
    228             final LayoutInflater inflater = LayoutInflater.from(context);
    229             mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
    230             mHintToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
    231             mHintToSaveText = context.getText(R.string.hint_add_to_dictionary);
    232         }
    233 
    234         private static Drawable getMoreSuggestionsHint(Resources res, float textSize, int color) {
    235             final Paint paint = new Paint();
    236             paint.setAntiAlias(true);
    237             paint.setTextAlign(Align.CENTER);
    238             paint.setTextSize(textSize);
    239             paint.setColor(color);
    240             final Rect bounds = new Rect();
    241             paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds);
    242             final int width = Math.round(bounds.width() + 0.5f);
    243             final int height = Math.round(bounds.height() + 0.5f);
    244             final Bitmap buffer = Bitmap.createBitmap(
    245                     width, (height * 3 / 2), Bitmap.Config.ARGB_8888);
    246             final Canvas canvas = new Canvas(buffer);
    247             canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint);
    248             return new BitmapDrawable(res, buffer);
    249         }
    250 
    251         // Read integer value in TypedArray as percent.
    252         private static float getPercent(TypedArray a, int index, int defValue) {
    253             return a.getInt(index, defValue) / 100.0f;
    254         }
    255 
    256         // Read fraction value in TypedArray as float.
    257         private static float getRatio(TypedArray a, int index) {
    258             return a.getFraction(index, 1000, 1000, 1) / 1000.0f;
    259         }
    260 
    261         private CharSequence getStyledSuggestionWord(SuggestedWords suggestions, int pos) {
    262             final CharSequence word = suggestions.getWord(pos);
    263             final boolean isAutoCorrect = pos == 1 && Utils.willAutoCorrect(suggestions);
    264             final boolean isTypedWordValid = pos == 0 && suggestions.mTypedWordValid;
    265             if (!isAutoCorrect && !isTypedWordValid)
    266                 return word;
    267 
    268             final int len = word.length();
    269             final Spannable spannedWord = new SpannableString(word);
    270             final int option = mSuggestionStripOption;
    271             if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0)
    272                     || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) {
    273                 spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    274             }
    275             if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) {
    276                 spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    277             }
    278             return spannedWord;
    279         }
    280 
    281         private int getWordPosition(int index, SuggestedWords suggestions) {
    282             // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
    283             // suggestions.
    284             final int centerPos = Utils.willAutoCorrect(suggestions) ? 1 : 0;
    285             if (index == mCenterSuggestionIndex) {
    286                 return centerPos;
    287             } else if (index == centerPos) {
    288                 return mCenterSuggestionIndex;
    289             } else {
    290                 return index;
    291             }
    292         }
    293 
    294         private int getSuggestionTextColor(int index, SuggestedWords suggestions, int pos) {
    295             // TODO: Need to revisit this logic with bigram suggestions
    296             final boolean isSuggested = (pos != 0);
    297 
    298             final int color;
    299             if (index == mCenterSuggestionIndex && Utils.willAutoCorrect(suggestions)) {
    300                 color = mColorAutoCorrect;
    301             } else if (isSuggested) {
    302                 color = mColorSuggested;
    303             } else {
    304                 color = mColorTypedWord;
    305             }
    306             if (LatinImeLogger.sDBG) {
    307                 if (index == mCenterSuggestionIndex && suggestions.mHasAutoCorrectionCandidate
    308                         && suggestions.shouldBlockAutoCorrection()) {
    309                     return 0xFFFF0000;
    310                 }
    311             }
    312 
    313             final SuggestedWordInfo info = (pos < suggestions.size())
    314                     ? suggestions.getInfo(pos) : null;
    315             if (info != null && info.isObsoleteSuggestedWord()) {
    316                 return applyAlpha(color, mAlphaObsoleted);
    317             } else {
    318                 return color;
    319             }
    320         }
    321 
    322         private static int applyAlpha(final int color, final float alpha) {
    323             final int newAlpha = (int)(Color.alpha(color) * alpha);
    324             return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color));
    325         }
    326 
    327         private static void addDivider(final ViewGroup stripView, final View divider) {
    328             stripView.addView(divider);
    329             final LinearLayout.LayoutParams params =
    330                     (LinearLayout.LayoutParams)divider.getLayoutParams();
    331             params.gravity = Gravity.CENTER;
    332         }
    333 
    334         public void layout(SuggestedWords suggestions, ViewGroup stripView, ViewGroup placer,
    335                 int stripWidth) {
    336             if (suggestions.isPunctuationSuggestions()) {
    337                 layoutPunctuationSuggestions(suggestions, stripView);
    338                 return;
    339             }
    340 
    341             final int countInStrip = mSuggestionsCountInStrip;
    342             setupTexts(suggestions, countInStrip);
    343             mMoreSuggestionsAvailable = (suggestions.size() > countInStrip);
    344             int x = 0;
    345             for (int index = 0; index < countInStrip; index++) {
    346                 final int pos = getWordPosition(index, suggestions);
    347 
    348                 if (index != 0) {
    349                     final View divider = mDividers.get(pos);
    350                     // Add divider if this isn't the left most suggestion in suggestions strip.
    351                     addDivider(stripView, divider);
    352                     x += divider.getMeasuredWidth();
    353                 }
    354 
    355                 final CharSequence styled = mTexts.get(pos);
    356                 final TextView word = mWords.get(pos);
    357                 if (index == mCenterSuggestionIndex && mMoreSuggestionsAvailable) {
    358                     // TODO: This "more suggestions hint" should have nicely designed icon.
    359                     word.setCompoundDrawablesWithIntrinsicBounds(
    360                             null, null, null, mMoreSuggestionsHint);
    361                     // HACK: To align with other TextView that has no compound drawables.
    362                     word.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
    363                 } else {
    364                     word.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
    365                 }
    366 
    367                 // Disable this suggestion if the suggestion is null or empty.
    368                 word.setEnabled(!TextUtils.isEmpty(styled));
    369                 word.setTextColor(getSuggestionTextColor(index, suggestions, pos));
    370                 final int width = getSuggestionWidth(index, stripWidth);
    371                 final CharSequence text = getEllipsizedText(styled, width, word.getPaint());
    372                 final float scaleX = word.getTextScaleX();
    373                 word.setText(text); // TextView.setText() resets text scale x to 1.0.
    374                 word.setTextScaleX(scaleX);
    375                 stripView.addView(word);
    376                 setLayoutWeight(
    377                         word, getSuggestionWeight(index), ViewGroup.LayoutParams.MATCH_PARENT);
    378                 x += word.getMeasuredWidth();
    379 
    380                 if (DBG) {
    381                     final CharSequence debugInfo = getDebugInfo(suggestions, pos);
    382                     if (debugInfo != null) {
    383                         final TextView info = mInfos.get(pos);
    384                         info.setText(debugInfo);
    385                         placer.addView(info);
    386                         info.measure(ViewGroup.LayoutParams.WRAP_CONTENT,
    387                                 ViewGroup.LayoutParams.WRAP_CONTENT);
    388                         final int infoWidth = info.getMeasuredWidth();
    389                         final int y = info.getMeasuredHeight();
    390                         FrameLayoutCompatUtils.placeViewAt(
    391                                 info, x - infoWidth, y, infoWidth, info.getMeasuredHeight());
    392                     }
    393                 }
    394             }
    395         }
    396 
    397         private int getSuggestionWidth(int index, int maxWidth) {
    398             final int paddings = mPadding * mSuggestionsCountInStrip;
    399             final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
    400             final int availableWidth = maxWidth - paddings - dividers;
    401             return (int)(availableWidth * getSuggestionWeight(index));
    402         }
    403 
    404         private float getSuggestionWeight(int index) {
    405             if (index == mCenterSuggestionIndex) {
    406                 return mCenterSuggestionWeight;
    407             } else {
    408                 // TODO: Revisit this for cases of 5 or more suggestions
    409                 return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1);
    410             }
    411         }
    412 
    413         private void setupTexts(SuggestedWords suggestions, int countInStrip) {
    414             mTexts.clear();
    415             final int count = Math.min(suggestions.size(), countInStrip);
    416             for (int pos = 0; pos < count; pos++) {
    417                 final CharSequence styled = getStyledSuggestionWord(suggestions, pos);
    418                 mTexts.add(styled);
    419             }
    420             for (int pos = count; pos < countInStrip; pos++) {
    421                 // Make this inactive for touches in layout().
    422                 mTexts.add(null);
    423             }
    424         }
    425 
    426         private void layoutPunctuationSuggestions(SuggestedWords suggestions, ViewGroup stripView) {
    427             final int countInStrip = Math.min(suggestions.size(), PUNCTUATIONS_IN_STRIP);
    428             for (int index = 0; index < countInStrip; index++) {
    429                 if (index != 0) {
    430                     // Add divider if this isn't the left most suggestion in suggestions strip.
    431                     addDivider(stripView, mDividers.get(index));
    432                 }
    433 
    434                 final TextView word = mWords.get(index);
    435                 word.setEnabled(true);
    436                 word.setTextColor(mColorTypedWord);
    437                 final CharSequence text = suggestions.getWord(index);
    438                 word.setText(text);
    439                 word.setTextScaleX(1.0f);
    440                 word.setCompoundDrawables(null, null, null, null);
    441                 stripView.addView(word);
    442                 setLayoutWeight(word, 1.0f, mSuggestionsStripHeight);
    443             }
    444             mMoreSuggestionsAvailable = false;
    445         }
    446 
    447         public void layoutAddToDictionaryHint(CharSequence word, ViewGroup stripView,
    448                 int stripWidth) {
    449             final int width = stripWidth - mDividerWidth - mPadding * 2;
    450 
    451             final TextView wordView = mWordToSaveView;
    452             wordView.setTextColor(mColorTypedWord);
    453             final int wordWidth = (int)(width * mCenterSuggestionWeight);
    454             final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
    455             final float wordScaleX = wordView.getTextScaleX();
    456             wordView.setTag(word);
    457             wordView.setText(text);
    458             wordView.setTextScaleX(wordScaleX);
    459             stripView.addView(wordView);
    460             setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
    461 
    462             stripView.addView(mDividers.get(0));
    463 
    464             final TextView hintView = mHintToSaveView;
    465             hintView.setTextColor(mColorAutoCorrect);
    466             final int hintWidth = width - wordWidth;
    467             final float hintScaleX = getTextScaleX(mHintToSaveText, hintWidth, hintView.getPaint());
    468             hintView.setText(mHintToSaveText);
    469             hintView.setTextScaleX(hintScaleX);
    470             stripView.addView(hintView);
    471             setLayoutWeight(
    472                     hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
    473         }
    474     }
    475 
    476     /**
    477      * Construct a {@link SuggestionsView} for showing suggested words for completion.
    478      * @param context
    479      * @param attrs
    480      */
    481     public SuggestionsView(Context context, AttributeSet attrs) {
    482         this(context, attrs, R.attr.suggestionsViewStyle);
    483     }
    484 
    485     public SuggestionsView(Context context, AttributeSet attrs, int defStyle) {
    486         super(context, attrs, defStyle);
    487 
    488         final LayoutInflater inflater = LayoutInflater.from(context);
    489         inflater.inflate(R.layout.suggestions_strip, this);
    490 
    491         mPreviewPopup = new PopupWindow(context);
    492         mPreviewText = (TextView) inflater.inflate(R.layout.suggestion_preview, null);
    493         mPreviewPopup.setWindowLayoutMode(
    494                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    495         mPreviewPopup.setContentView(mPreviewText);
    496         mPreviewPopup.setBackgroundDrawable(null);
    497 
    498         mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip);
    499         for (int pos = 0; pos < MAX_SUGGESTIONS; pos++) {
    500             final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null);
    501             word.setTag(pos);
    502             word.setOnClickListener(this);
    503             word.setOnLongClickListener(this);
    504             mWords.add(word);
    505             final View divider = inflater.inflate(R.layout.suggestion_divider, null);
    506             divider.setTag(pos);
    507             divider.setOnClickListener(this);
    508             mDividers.add(divider);
    509             mInfos.add((TextView)inflater.inflate(R.layout.suggestion_info, null));
    510         }
    511 
    512         mParams = new SuggestionsViewParams(context, attrs, defStyle, mWords, mDividers, mInfos);
    513         mParams.mWordToSaveView.setOnClickListener(this);
    514 
    515         mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null);
    516         mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer
    517                 .findViewById(R.id.more_suggestions_view);
    518         mMoreSuggestionsBuilder = new MoreSuggestions.Builder(mMoreSuggestionsView);
    519 
    520         final PopupWindow moreWindow = new PopupWindow(context);
    521         moreWindow.setWindowLayoutMode(
    522                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    523         moreWindow.setBackgroundDrawable(new ColorDrawable(android.R.color.transparent));
    524         moreWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
    525         moreWindow.setFocusable(true);
    526         moreWindow.setOutsideTouchable(true);
    527         moreWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
    528             @Override
    529             public void onDismiss() {
    530                 mKeyboardView.dimEntireKeyboard(false);
    531             }
    532         });
    533         mMoreSuggestionsWindow = moreWindow;
    534 
    535         final Resources res = context.getResources();
    536         mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset(
    537                 R.dimen.more_suggestions_modal_tolerance);
    538         mMoreSuggestionsSlidingDetector = new GestureDetector(
    539                 context, mMoreSuggestionsSlidingListener);
    540     }
    541 
    542     /**
    543      * A connection back to the input method.
    544      * @param listener
    545      */
    546     public void setListener(Listener listener, View inputView) {
    547         mListener = listener;
    548         mKeyboardView = (KeyboardView)inputView.findViewById(R.id.keyboard_view);
    549     }
    550 
    551     public void setSuggestions(SuggestedWords suggestions) {
    552         if (suggestions == null || suggestions.size() == 0)
    553             return;
    554 
    555         clear();
    556         mSuggestions = suggestions;
    557         mParams.layout(mSuggestions, mSuggestionsStrip, this, getWidth());
    558     }
    559 
    560     private static CharSequence getDebugInfo(SuggestedWords suggestions, int pos) {
    561         if (DBG && pos < suggestions.size()) {
    562             final SuggestedWordInfo wordInfo = suggestions.getInfo(pos);
    563             if (wordInfo != null) {
    564                 final CharSequence debugInfo = wordInfo.getDebugString();
    565                 if (!TextUtils.isEmpty(debugInfo)) {
    566                     return debugInfo;
    567                 }
    568             }
    569         }
    570         return null;
    571     }
    572 
    573     private static void setLayoutWeight(View v, float weight, int height) {
    574         final ViewGroup.LayoutParams lp = v.getLayoutParams();
    575         if (lp instanceof LinearLayout.LayoutParams) {
    576             final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp;
    577             llp.weight = weight;
    578             llp.width = 0;
    579             llp.height = height;
    580         }
    581     }
    582 
    583     private static float getTextScaleX(CharSequence text, int maxWidth, TextPaint paint) {
    584         paint.setTextScaleX(1.0f);
    585         final int width = getTextWidth(text, paint);
    586         if (width <= maxWidth) {
    587             return 1.0f;
    588         }
    589         return maxWidth / (float)width;
    590     }
    591 
    592     private static CharSequence getEllipsizedText(CharSequence text, int maxWidth,
    593             TextPaint paint) {
    594         if (text == null) return null;
    595         paint.setTextScaleX(1.0f);
    596         final int width = getTextWidth(text, paint);
    597         if (width <= maxWidth) {
    598             return text;
    599         }
    600         final float scaleX = maxWidth / (float)width;
    601         if (scaleX >= MIN_TEXT_XSCALE) {
    602             paint.setTextScaleX(scaleX);
    603             return text;
    604         }
    605 
    606         // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To get
    607         // squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE).
    608         final CharSequence ellipsized = TextUtils.ellipsize(
    609                 text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE);
    610         paint.setTextScaleX(MIN_TEXT_XSCALE);
    611         return ellipsized;
    612     }
    613 
    614     private static int getTextWidth(CharSequence text, TextPaint paint) {
    615         if (TextUtils.isEmpty(text)) return 0;
    616         final Typeface savedTypeface = paint.getTypeface();
    617         paint.setTypeface(getTextTypeface(text));
    618         final int len = text.length();
    619         final float[] widths = new float[len];
    620         final int count = paint.getTextWidths(text, 0, len, widths);
    621         int width = 0;
    622         for (int i = 0; i < count; i++) {
    623             width += Math.round(widths[i] + 0.5f);
    624         }
    625         paint.setTypeface(savedTypeface);
    626         return width;
    627     }
    628 
    629     private static Typeface getTextTypeface(CharSequence text) {
    630         if (!(text instanceof SpannableString))
    631             return Typeface.DEFAULT;
    632 
    633         final SpannableString ss = (SpannableString)text;
    634         final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class);
    635         if (styles.length == 0)
    636             return Typeface.DEFAULT;
    637 
    638         switch (styles[0].getStyle()) {
    639         case Typeface.BOLD: return Typeface.DEFAULT_BOLD;
    640         // TODO: BOLD_ITALIC, ITALIC case?
    641         default: return Typeface.DEFAULT;
    642         }
    643     }
    644 
    645     public boolean isShowingAddToDictionaryHint() {
    646         return mSuggestionsStrip.getChildCount() > 0
    647                 && mSuggestionsStrip.getChildAt(0) == mParams.mWordToSaveView;
    648     }
    649 
    650     public void showAddToDictionaryHint(CharSequence word) {
    651         clear();
    652         mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth());
    653     }
    654 
    655     public boolean dismissAddToDictionaryHint() {
    656         if (isShowingAddToDictionaryHint()) {
    657             clear();
    658             return true;
    659         }
    660         return false;
    661     }
    662 
    663     public SuggestedWords getSuggestions() {
    664         return mSuggestions;
    665     }
    666 
    667     public void clear() {
    668         mSuggestionsStrip.removeAllViews();
    669         removeAllViews();
    670         addView(mSuggestionsStrip);
    671         dismissMoreSuggestions();
    672     }
    673 
    674     private void hidePreview() {
    675         mPreviewPopup.dismiss();
    676     }
    677 
    678     private void showPreview(View view, CharSequence word) {
    679         if (TextUtils.isEmpty(word))
    680             return;
    681 
    682         final TextView previewText = mPreviewText;
    683         previewText.setTextColor(mParams.mColorTypedWord);
    684         previewText.setText(word);
    685         previewText.measure(
    686                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    687         final int[] offsetInWindow = new int[2];
    688         view.getLocationInWindow(offsetInWindow);
    689         final int posX = offsetInWindow[0];
    690         final int posY = offsetInWindow[1] - previewText.getMeasuredHeight();
    691         final PopupWindow previewPopup = mPreviewPopup;
    692         if (previewPopup.isShowing()) {
    693             previewPopup.update(posX, posY, previewPopup.getWidth(), previewPopup.getHeight());
    694         } else {
    695             previewPopup.showAtLocation(this, Gravity.NO_GRAVITY, posX, posY);
    696         }
    697         previewText.setVisibility(VISIBLE);
    698         mHandler.postHidePreview();
    699     }
    700 
    701     private void addToDictionary(CharSequence word) {
    702         if (mListener.addWordToDictionary(word.toString())) {
    703             final CharSequence message = getContext().getString(R.string.added_word, word);
    704             showPreview(mParams.mWordToSaveView, message);
    705         }
    706     }
    707 
    708     private final KeyboardActionListener mMoreSuggestionsListener =
    709             new KeyboardActionListener.Adapter() {
    710         @Override
    711         public boolean onCustomRequest(int requestCode) {
    712             final int index = requestCode;
    713             final CharSequence word = mSuggestions.getWord(index);
    714             mListener.pickSuggestionManually(index, word);
    715             dismissMoreSuggestions();
    716             return true;
    717         }
    718 
    719         @Override
    720         public void onCancelInput() {
    721             dismissMoreSuggestions();
    722         }
    723     };
    724 
    725     private final MoreKeysPanel.Controller mMoreSuggestionsController =
    726             new MoreKeysPanel.Controller() {
    727         @Override
    728         public boolean dismissMoreKeysPanel() {
    729             return dismissMoreSuggestions();
    730         }
    731     };
    732 
    733     private boolean dismissMoreSuggestions() {
    734         if (mMoreSuggestionsWindow.isShowing()) {
    735             mMoreSuggestionsWindow.dismiss();
    736             return true;
    737         }
    738         return false;
    739     }
    740 
    741     public boolean handleBack() {
    742         return dismissMoreSuggestions();
    743     }
    744 
    745     @Override
    746     public boolean onLongClick(View view) {
    747         return showMoreSuggestions();
    748     }
    749 
    750     private boolean showMoreSuggestions() {
    751         final SuggestionsViewParams params = mParams;
    752         if (params.mMoreSuggestionsAvailable) {
    753             final int stripWidth = getWidth();
    754             final View container = mMoreSuggestionsContainer;
    755             final int maxWidth = stripWidth - container.getPaddingLeft()
    756                     - container.getPaddingRight();
    757             final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder;
    758             builder.layout(mSuggestions, params.mSuggestionsCountInStrip, maxWidth,
    759                     (int)(maxWidth * params.mMinMoreSuggestionsWidth),
    760                     params.mMaxMoreSuggestionsRow);
    761             mMoreSuggestionsView.setKeyboard(builder.build());
    762             container.measure(
    763                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    764 
    765             final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
    766             final int pointX = stripWidth / 2;
    767             final int pointY = -params.mMoreSuggestionsBottomGap;
    768             moreKeysPanel.showMoreKeysPanel(
    769                     this, mMoreSuggestionsController, pointX, pointY,
    770                     mMoreSuggestionsWindow, mMoreSuggestionsListener);
    771             mMoreSuggestionsMode = MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING;
    772             mOriginX = mLastX;
    773             mOriginY = mLastY;
    774             mKeyboardView.dimEntireKeyboard(true);
    775             for (int i = 0; i < params.mSuggestionsCountInStrip; i++) {
    776                 mWords.get(i).setPressed(false);
    777             }
    778             return true;
    779         }
    780         return false;
    781     }
    782 
    783     // Working variables for onLongClick and dispatchTouchEvent.
    784     private int mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE;
    785     private static final int MORE_SUGGESTIONS_IN_MODAL_MODE = 0;
    786     private static final int MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING = 1;
    787     private static final int MORE_SUGGESTIONS_IN_SLIDING_MODE = 2;
    788     private int mLastX;
    789     private int mLastY;
    790     private int mOriginX;
    791     private int mOriginY;
    792     private final int mMoreSuggestionsModalTolerance;
    793     private final GestureDetector mMoreSuggestionsSlidingDetector;
    794     private final GestureDetector.OnGestureListener mMoreSuggestionsSlidingListener =
    795             new GestureDetector.SimpleOnGestureListener() {
    796         @Override
    797         public boolean onScroll(MotionEvent down, MotionEvent me, float deltaX, float deltaY) {
    798             final float dy = me.getY() - down.getY();
    799             if (deltaY > 0 && dy < 0) {
    800                 return showMoreSuggestions();
    801             }
    802             return false;
    803         }
    804     };
    805 
    806     @Override
    807     public boolean dispatchTouchEvent(MotionEvent me) {
    808         if (!mMoreSuggestionsWindow.isShowing()
    809                 || mMoreSuggestionsMode == MORE_SUGGESTIONS_IN_MODAL_MODE) {
    810             mLastX = (int)me.getX();
    811             mLastY = (int)me.getY();
    812             if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) {
    813                 return true;
    814             }
    815             return super.dispatchTouchEvent(me);
    816         }
    817 
    818         final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView;
    819         final int action = me.getAction();
    820         final long eventTime = me.getEventTime();
    821         final int index = me.getActionIndex();
    822         final int id = me.getPointerId(index);
    823         final PointerTracker tracker = PointerTracker.getPointerTracker(id, moreKeysPanel);
    824         final int x = (int)me.getX(index);
    825         final int y = (int)me.getY(index);
    826         final int translatedX = moreKeysPanel.translateX(x);
    827         final int translatedY = moreKeysPanel.translateY(y);
    828 
    829         if (mMoreSuggestionsMode == MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING) {
    830             if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance
    831                     || mOriginY - y >= mMoreSuggestionsModalTolerance) {
    832                 // Decided to be in the sliding input mode only when the touch point has been moved
    833                 // upward.
    834                 mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_SLIDING_MODE;
    835                 tracker.onShowMoreKeysPanel(
    836                         translatedX, translatedY, SystemClock.uptimeMillis(), moreKeysPanel);
    837             } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
    838                 // Decided to be in the modal input mode
    839                 mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE;
    840             }
    841             return true;
    842         }
    843 
    844         // MORE_SUGGESTIONS_IN_SLIDING_MODE
    845         tracker.processMotionEvent(action, translatedX, translatedY, eventTime, moreKeysPanel);
    846         return true;
    847     }
    848 
    849     @Override
    850     public void onClick(View view) {
    851         if (view == mParams.mWordToSaveView) {
    852             addToDictionary((CharSequence)view.getTag());
    853             clear();
    854             return;
    855         }
    856 
    857         final Object tag = view.getTag();
    858         if (!(tag instanceof Integer))
    859             return;
    860         final int index = (Integer) tag;
    861         if (index >= mSuggestions.size())
    862             return;
    863 
    864         final CharSequence word = mSuggestions.getWord(index);
    865         mListener.pickSuggestionManually(index, word);
    866     }
    867 
    868     @Override
    869     protected void onDetachedFromWindow() {
    870         super.onDetachedFromWindow();
    871         mHandler.cancelAllMessages();
    872         hidePreview();
    873     }
    874 }
    875