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