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