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