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