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