SuggestionStripView.java revision 16713e5630b93fb5625df26745eb73271f189457
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.content.res.TypedArray; 22import android.graphics.Color; 23import android.graphics.Typeface; 24import android.graphics.drawable.Drawable; 25import android.os.Message; 26import android.text.Spannable; 27import android.text.SpannableString; 28import android.text.Spanned; 29import android.text.TextPaint; 30import android.text.TextUtils; 31import android.text.style.BackgroundColorSpan; 32import android.text.style.CharacterStyle; 33import android.text.style.ForegroundColorSpan; 34import android.text.style.StyleSpan; 35import android.text.style.UnderlineSpan; 36import android.util.AttributeSet; 37import android.view.Gravity; 38import android.view.LayoutInflater; 39import android.view.View; 40import android.view.View.OnClickListener; 41import android.view.ViewGroup; 42import android.widget.LinearLayout; 43import android.widget.PopupWindow; 44import android.widget.TextView; 45 46import com.android.inputmethod.compat.FrameLayoutCompatUtils; 47import com.android.inputmethod.compat.LinearLayoutCompatUtils; 48import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 49 50import java.util.ArrayList; 51import java.util.List; 52 53public class CandidateView extends LinearLayout implements OnClickListener { 54 55 public interface Listener { 56 public boolean addWordToDictionary(String word); 57 public void pickSuggestionManually(int index, CharSequence word); 58 } 59 60 // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}. 61 private static final int MAX_SUGGESTIONS = 18; 62 private static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT; 63 private static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT; 64 65 private static final boolean DBG = LatinImeLogger.sDBG; 66 67 private final ViewGroup mCandidatesStrip; 68 private final ViewGroup mCandidatesPaneControl; 69 private final TextView mExpandCandidatesPane; 70 private final TextView mCloseCandidatesPane; 71 private ViewGroup mCandidatesPane; 72 private ViewGroup mCandidatesPaneContainer; 73 private View mKeyboardView; 74 75 private final ArrayList<TextView> mWords = new ArrayList<TextView>(); 76 private final ArrayList<TextView> mInfos = new ArrayList<TextView>(); 77 private final ArrayList<View> mDividers = new ArrayList<View>(); 78 79 private final PopupWindow mPreviewPopup; 80 private final TextView mPreviewText; 81 82 private final View mTouchToSave; 83 private final TextView mWordToSave; 84 85 private Listener mListener; 86 private SuggestedWords mSuggestions = SuggestedWords.EMPTY; 87 private boolean mShowingAutoCorrectionInverted; 88 private boolean mShowingAddToDictionary; 89 90 private final SuggestionsStripParams mStripParams; 91 private final SuggestionsPaneParams mPaneParams; 92 private static final float MIN_TEXT_XSCALE = 0.75f; 93 94 private final UiHandler mHandler = new UiHandler(this); 95 96 private static class UiHandler extends StaticInnerHandlerWrapper<CandidateView> { 97 private static final int MSG_HIDE_PREVIEW = 0; 98 private static final int MSG_UPDATE_SUGGESTION = 1; 99 100 private static final long DELAY_HIDE_PREVIEW = 1000; 101 private static final long DELAY_UPDATE_SUGGESTION = 300; 102 103 public UiHandler(CandidateView outerInstance) { 104 super(outerInstance); 105 } 106 107 @Override 108 public void dispatchMessage(Message msg) { 109 final CandidateView candidateView = getOuterInstance(); 110 switch (msg.what) { 111 case MSG_HIDE_PREVIEW: 112 candidateView.hidePreview(); 113 break; 114 case MSG_UPDATE_SUGGESTION: 115 candidateView.updateSuggestions(); 116 break; 117 } 118 } 119 120 public void postHidePreview() { 121 cancelHidePreview(); 122 sendMessageDelayed(obtainMessage(MSG_HIDE_PREVIEW), DELAY_HIDE_PREVIEW); 123 } 124 125 public void cancelHidePreview() { 126 removeMessages(MSG_HIDE_PREVIEW); 127 } 128 129 public void postUpdateSuggestions() { 130 cancelUpdateSuggestions(); 131 sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION), 132 DELAY_UPDATE_SUGGESTION); 133 } 134 135 public void cancelUpdateSuggestions() { 136 removeMessages(MSG_UPDATE_SUGGESTION); 137 } 138 139 public void cancelAllMessages() { 140 cancelHidePreview(); 141 cancelUpdateSuggestions(); 142 } 143 } 144 145 private static class CandidateViewParams { 146 public final int mPadding; 147 public final int mDividerWidth; 148 public final int mDividerHeight; 149 public final int mControlWidth; 150 public final int mCandidateStripHeight; 151 152 protected final List<TextView> mWords; 153 protected final List<View> mDividers; 154 protected final List<TextView> mInfos; 155 156 protected CandidateViewParams(List<TextView> words, List<View> dividers, 157 List<TextView> infos, View control) { 158 mWords = words; 159 mDividers = dividers; 160 mInfos = infos; 161 162 final TextView word = words.get(0); 163 final View divider = dividers.get(0); 164 mPadding = word.getCompoundPaddingLeft() + word.getCompoundPaddingRight(); 165 divider.measure(WRAP_CONTENT, MATCH_PARENT); 166 mDividerWidth = divider.getMeasuredWidth(); 167 mDividerHeight = divider.getMeasuredHeight(); 168 mControlWidth = control.getMeasuredWidth(); 169 170 final Resources res = word.getResources(); 171 mCandidateStripHeight = res.getDimensionPixelOffset(R.dimen.candidate_strip_height); 172 } 173 } 174 175 private static class SuggestionsPaneParams extends CandidateViewParams { 176 public SuggestionsPaneParams(List<TextView> words, List<View> dividers, 177 List<TextView> infos, View control) { 178 super(words, dividers, infos, control); 179 } 180 181 public int layout(SuggestedWords suggestions, ViewGroup paneView, int from, int textColor, 182 int paneWidth) { 183 final int count = Math.min(mWords.size(), suggestions.size()); 184 View centeringFrom = null, lastView = null; 185 int x = 0, y = 0; 186 for (int index = from; index < count; index++) { 187 final int pos = index; 188 final TextView word = mWords.get(pos); 189 final View divider = mDividers.get(pos); 190 final TextPaint paint = word.getPaint(); 191 word.setTextColor(textColor); 192 final CharSequence styled = suggestions.getWord(pos); 193 194 final TextView info; 195 if (DBG) { 196 final CharSequence debugInfo = getDebugInfo(suggestions, index); 197 if (debugInfo != null) { 198 info = mInfos.get(index); 199 info.setText(debugInfo); 200 } else { 201 info = null; 202 } 203 } else { 204 info = null; 205 } 206 207 final CharSequence text; 208 final float scaleX; 209 paint.setTextScaleX(1.0f); 210 final int textWidth = getTextWidth(styled, paint); 211 int available = paneWidth - x - mPadding; 212 if (textWidth >= available) { 213 // Needs new row, centering previous row. 214 centeringCandidates(paneView, centeringFrom, lastView, x, paneWidth); 215 x = 0; 216 y += mCandidateStripHeight; 217 } 218 if (x != 0) { 219 // Add divider if this isn't the left most suggestion in current row. 220 paneView.addView(divider); 221 FrameLayoutCompatUtils.placeViewAt(divider, x, y 222 + (mCandidateStripHeight - mDividerHeight) / 2, mDividerWidth, 223 mDividerHeight); 224 x += mDividerWidth; 225 } 226 available = paneWidth - x - mPadding; 227 text = getEllipsizedText(styled, available, paint); 228 scaleX = paint.getTextScaleX(); 229 word.setText(text); 230 word.setTextScaleX(scaleX); 231 paneView.addView(word); 232 lastView = word; 233 if (x == 0) 234 centeringFrom = word; 235 word.measure(WRAP_CONTENT, 236 MeasureSpec.makeMeasureSpec(mCandidateStripHeight, MeasureSpec.EXACTLY)); 237 final int width = word.getMeasuredWidth(); 238 final int height = word.getMeasuredHeight(); 239 FrameLayoutCompatUtils.placeViewAt(word, x, y + (mCandidateStripHeight - height) 240 / 2, width, height); 241 x += width; 242 if (info != null) { 243 paneView.addView(info); 244 lastView = info; 245 info.measure(WRAP_CONTENT, WRAP_CONTENT); 246 final int infoWidth = info.getMeasuredWidth(); 247 FrameLayoutCompatUtils.placeViewAt(info, x - infoWidth, y, infoWidth, 248 info.getMeasuredHeight()); 249 } 250 } 251 if (x != 0) { 252 // Centering last candidates row. 253 centeringCandidates(paneView, centeringFrom, lastView, x, paneWidth); 254 } 255 256 return count - from; 257 } 258 } 259 260 private static class SuggestionsStripParams extends CandidateViewParams { 261 private static final int DEFAULT_CANDIDATE_COUNT_IN_STRIP = 3; 262 private static final int PUNCTUATIONS_IN_STRIP = 6; 263 264 private final int mColorTypedWord; 265 private final int mColorAutoCorrect; 266 private final int mColorSuggestedCandidate; 267 private final int mCandidateCountInStrip; 268 269 private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); 270 private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); 271 private final CharacterStyle mInvertedForegroundColorSpan; 272 private final CharacterStyle mInvertedBackgroundColorSpan; 273 private static final int AUTO_CORRECT_BOLD = 0x01; 274 private static final int AUTO_CORRECT_UNDERLINE = 0x02; 275 private static final int AUTO_CORRECT_INVERT = 0x04; 276 277 private final TextPaint mPaint; 278 private final int mAutoCorrectHighlight; 279 280 private final ArrayList<CharSequence> mTexts = new ArrayList<CharSequence>(); 281 private SuggestedWords mSuggestedWords; 282 283 private int mCountInStrip; 284 // True if the mCountInStrip suggestions can fit in suggestion strip in equally divided 285 // width without squeezing the text. 286 private boolean mCanUseFixedWidthColumns; 287 private int mMaxWidth; 288 private int mAvailableWidthForWords; 289 private int mConstantWidthForPaddings; 290 private int mVariableWidthForWords; 291 private float mScaleX; 292 293 public SuggestionsStripParams(Context context, AttributeSet attrs, int defStyle, 294 List<TextView> words, List<View> dividers, List<TextView> infos, View control) { 295 super(words, dividers, infos, control); 296 final TypedArray a = context.obtainStyledAttributes( 297 attrs, R.styleable.CandidateView, defStyle, R.style.CandidateViewStyle); 298 mAutoCorrectHighlight = a.getInt(R.styleable.CandidateView_autoCorrectHighlight, 0); 299 mColorTypedWord = a.getColor(R.styleable.CandidateView_colorTypedWord, 0); 300 mColorAutoCorrect = a.getColor(R.styleable.CandidateView_colorAutoCorrect, 0); 301 mColorSuggestedCandidate = a.getColor(R.styleable.CandidateView_colorSuggested, 0); 302 mCandidateCountInStrip = a.getInt( 303 R.styleable.CandidateView_candidateCountInStrip, 304 DEFAULT_CANDIDATE_COUNT_IN_STRIP); 305 a.recycle(); 306 307 mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorTypedWord ^ 0x00ffffff); 308 mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorTypedWord); 309 310 mPaint = new TextPaint(); 311 final float textSize = context.getResources().getDimension(R.dimen.candidate_text_size); 312 mPaint.setTextSize(textSize); 313 } 314 315 public int getTextColor() { 316 return mColorTypedWord; 317 } 318 319 private CharSequence getStyledCandidateWord(CharSequence word, boolean isAutoCorrect) { 320 if (!isAutoCorrect) 321 return word; 322 final int len = word.length(); 323 final Spannable spannedWord = new SpannableString(word); 324 if ((mAutoCorrectHighlight & AUTO_CORRECT_BOLD) != 0) 325 spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 326 if ((mAutoCorrectHighlight & AUTO_CORRECT_UNDERLINE) != 0) 327 spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 328 return spannedWord; 329 } 330 331 private int getWordPosition(int index) { 332 if (index >= 2) { 333 return index; 334 } 335 final boolean willAutoCorrect = !mSuggestedWords.mTypedWordValid 336 && mSuggestedWords.mHasMinimalSuggestion; 337 return willAutoCorrect ? 1 - index : index; 338 } 339 340 private int getCandidateTextColor(int pos) { 341 final SuggestedWords suggestions = mSuggestedWords; 342 final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion 343 && ((pos == 1 && !suggestions.mTypedWordValid) 344 || (pos == 0 && suggestions.mTypedWordValid)); 345 // TODO: Need to revisit this logic with bigram suggestions 346 final boolean isSuggestedCandidate = (pos != 0); 347 final boolean isPunctuationSuggestions = suggestions.isPunctuationSuggestions(); 348 349 final int color; 350 if (isPunctuationSuggestions) { 351 color = mColorTypedWord; 352 } else if (isAutoCorrect) { 353 color = mColorAutoCorrect; 354 } else if (isSuggestedCandidate) { 355 color = mColorSuggestedCandidate; 356 } else { 357 color = mColorTypedWord; 358 } 359 final SuggestedWordInfo info = suggestions.getInfo(pos); 360 if (info != null && info.isPreviousSuggestedWord()) { 361 return applyAlpha(color, 0.5f); 362 } else { 363 return color; 364 } 365 } 366 367 private static int applyAlpha(final int color, final float alpha) { 368 final int newAlpha = (int)(Color.alpha(color) * alpha); 369 return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); 370 } 371 372 public CharSequence getInvertedText(CharSequence text) { 373 if ((mAutoCorrectHighlight & AUTO_CORRECT_INVERT) == 0) 374 return null; 375 final int len = text.length(); 376 final Spannable word = new SpannableString(text); 377 word.setSpan(mInvertedBackgroundColorSpan, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 378 word.setSpan(mInvertedForegroundColorSpan, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 379 return word; 380 } 381 382 public int layout(SuggestedWords suggestions, ViewGroup stripView, ViewGroup paneView, 383 int stripWidth) { 384 mSuggestedWords = suggestions; 385 final int maxCount = suggestions.isPunctuationSuggestions() 386 ? PUNCTUATIONS_IN_STRIP : mCandidateCountInStrip; 387 final int size = suggestions.size(); 388 setupTexts(suggestions, size); 389 mCountInStrip = Math.min(maxCount, size); 390 mScaleX = 1.0f; 391 calculateParameters(size, stripWidth); 392 393 int infoX = 0; 394 for (int index = 0; index < mCountInStrip; index++) { 395 final int pos = getWordPosition(index); 396 final TextView word = mWords.get(pos); 397 final View divider = mDividers.get(pos); 398 final TextPaint paint = word.getPaint(); 399 // TODO: Reorder candidates in strip as appropriate. The center candidate should 400 // hold the word when space is typed (valid typed word or auto corrected word). 401 word.setTextColor(getCandidateTextColor(pos)); 402 final CharSequence styled = mTexts.get(pos); 403 404 final TextView info; 405 if (DBG) { 406 final CharSequence debugInfo = getDebugInfo(mSuggestedWords, index); 407 if (debugInfo != null) { 408 info = mInfos.get(index); 409 info.setText(debugInfo); 410 } else { 411 info = null; 412 } 413 } else { 414 info = null; 415 } 416 417 final CharSequence text; 418 final float scaleX; 419 if (index == 0 && mCountInStrip == 1) { 420 text = getEllipsizedText(styled, mMaxWidth, paint); 421 scaleX = paint.getTextScaleX(); 422 } else { 423 text = styled; 424 scaleX = mScaleX; 425 } 426 word.setText(text); 427 word.setTextScaleX(scaleX); 428 if (index != 0) { 429 // Add divider if this isn't the left most suggestion in candidate strip. 430 stripView.addView(divider); 431 } 432 stripView.addView(word); 433 if (mCanUseFixedWidthColumns) { 434 setLayoutWeight(word, 1.0f, mCandidateStripHeight); 435 } else { 436 final int width = getTextWidth(text, paint) + mPadding; 437 setLayoutWeight(word, width, mCandidateStripHeight); 438 } 439 if (info != null) { 440 paneView.addView(info); 441 info.measure(WRAP_CONTENT, WRAP_CONTENT); 442 final int width = info.getMeasuredWidth(); 443 final int y = info.getMeasuredHeight(); 444 FrameLayoutCompatUtils.placeViewAt(info, infoX, 0, width, y); 445 infoX += width * 2; 446 } 447 } 448 449 return mCountInStrip; 450 } 451 452 private void calculateParameters(int size, int maxWidth) { 453 do { 454 mMaxWidth = maxWidth; 455 if (size > mCountInStrip) { 456 mMaxWidth -= mControlWidth; 457 } 458 459 tryLayout(); 460 461 if (mCanUseFixedWidthColumns) { 462 return; 463 } 464 if (mVariableWidthForWords <= mAvailableWidthForWords) { 465 return; 466 } 467 468 final float scaleX = mAvailableWidthForWords / (float)mVariableWidthForWords; 469 if (scaleX >= MIN_TEXT_XSCALE) { 470 mScaleX = scaleX; 471 return; 472 } 473 474 mCountInStrip--; 475 } while (mCountInStrip > 1); 476 } 477 478 private void tryLayout() { 479 final int maxCount = mCountInStrip; 480 final int dividers = mDividerWidth * (maxCount - 1); 481 mConstantWidthForPaddings = dividers + mPadding * maxCount; 482 mAvailableWidthForWords = mMaxWidth - mConstantWidthForPaddings; 483 484 mPaint.setTextScaleX(mScaleX); 485 final int maxFixedWidthForWord = (mMaxWidth - dividers) / maxCount - mPadding; 486 mCanUseFixedWidthColumns = true; 487 mVariableWidthForWords = 0; 488 for (int i = 0; i < maxCount; i++) { 489 final int width = getTextWidth(mTexts.get(i), mPaint); 490 if (width > maxFixedWidthForWord) 491 mCanUseFixedWidthColumns = false; 492 mVariableWidthForWords += width; 493 } 494 } 495 496 private void setupTexts(SuggestedWords suggestions, int count) { 497 mTexts.clear(); 498 for (int i = 0; i < count; i++) { 499 final CharSequence word = suggestions.getWord(i); 500 final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion 501 && ((i == 1 && !suggestions.mTypedWordValid) 502 || (i == 0 && suggestions.mTypedWordValid)); 503 final CharSequence styled = getStyledCandidateWord(word, isAutoCorrect); 504 mTexts.add(styled); 505 } 506 } 507 508 @Override 509 public String toString() { 510 return String.format( 511 "count=%d width=%d avail=%d fixcol=%s scaleX=%4.2f const=%d var=%d", 512 mCountInStrip, mMaxWidth, mAvailableWidthForWords, mCanUseFixedWidthColumns, 513 mScaleX, mConstantWidthForPaddings, mVariableWidthForWords); 514 } 515 } 516 517 /** 518 * Construct a CandidateView for showing suggested words for completion. 519 * @param context 520 * @param attrs 521 */ 522 public CandidateView(Context context, AttributeSet attrs) { 523 this(context, attrs, R.attr.candidateViewStyle); 524 } 525 526 public CandidateView(Context context, AttributeSet attrs, int defStyle) { 527 // Note: Up to version 10 (Gingerbread) of the API, LinearLayout doesn't have 3-argument 528 // constructor. 529 // TODO: Call 3-argument constructor, super(context, attrs, defStyle), when we abandon 530 // backward compatibility with the version 10 or earlier of the API. 531 super(context, attrs); 532 if (defStyle != R.attr.candidateViewStyle) { 533 throw new IllegalArgumentException( 534 "can't accept defStyle other than R.attr.candidayeViewStyle: defStyle=" 535 + defStyle); 536 } 537 setBackgroundDrawable(LinearLayoutCompatUtils.getBackgroundDrawable( 538 context, attrs, defStyle, R.style.CandidateViewStyle)); 539 540 final LayoutInflater inflater = LayoutInflater.from(context); 541 inflater.inflate(R.layout.candidates_strip, this); 542 543 mPreviewPopup = new PopupWindow(context); 544 mPreviewText = (TextView) inflater.inflate(R.layout.candidate_preview, null); 545 mPreviewPopup.setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, 546 ViewGroup.LayoutParams.WRAP_CONTENT); 547 mPreviewPopup.setContentView(mPreviewText); 548 mPreviewPopup.setBackgroundDrawable(null); 549 550 mCandidatesStrip = (ViewGroup)findViewById(R.id.candidates_strip); 551 for (int i = 0; i < MAX_SUGGESTIONS; i++) { 552 final TextView word = (TextView)inflater.inflate(R.layout.candidate_word, null); 553 word.setTag(i); 554 word.setOnClickListener(this); 555 mWords.add(word); 556 final View divider = inflater.inflate(R.layout.candidate_divider, null); 557 divider.setTag(i); 558 divider.setOnClickListener(this); 559 mDividers.add(divider); 560 mInfos.add((TextView)inflater.inflate(R.layout.candidate_info, null)); 561 } 562 563 mTouchToSave = findViewById(R.id.touch_to_save); 564 mWordToSave = (TextView)findViewById(R.id.word_to_save); 565 mWordToSave.setOnClickListener(this); 566 567 final TypedArray keyboardViewAttr = context.obtainStyledAttributes( 568 attrs, R.styleable.KeyboardView, R.attr.keyboardViewStyle, R.style.KeyboardView); 569 final Drawable expandBackground = keyboardViewAttr.getDrawable( 570 R.styleable.KeyboardView_keyBackground); 571 final Drawable closeBackground = keyboardViewAttr.getDrawable( 572 R.styleable.KeyboardView_keyBackground); 573 final int keyTextColor = keyboardViewAttr.getColor( 574 R.styleable.KeyboardView_keyTextColor, 0xFF000000); 575 keyboardViewAttr.recycle(); 576 577 mCandidatesPaneControl = (ViewGroup)findViewById(R.id.candidates_pane_control); 578 mExpandCandidatesPane = (TextView)findViewById(R.id.expand_candidates_pane); 579 mExpandCandidatesPane.setBackgroundDrawable(expandBackground); 580 mExpandCandidatesPane.setTextColor(keyTextColor); 581 mExpandCandidatesPane.setOnClickListener(new OnClickListener() { 582 @Override 583 public void onClick(View view) { 584 expandCandidatesPane(); 585 } 586 }); 587 mCloseCandidatesPane = (TextView)findViewById(R.id.close_candidates_pane); 588 mCloseCandidatesPane.setBackgroundDrawable(closeBackground); 589 mCloseCandidatesPane.setTextColor(keyTextColor); 590 mCloseCandidatesPane.setOnClickListener(new OnClickListener() { 591 @Override 592 public void onClick(View view) { 593 closeCandidatesPane(); 594 } 595 }); 596 mCandidatesPaneControl.measure(WRAP_CONTENT, WRAP_CONTENT); 597 598 mStripParams = new SuggestionsStripParams(context, attrs, defStyle, 599 mWords, mDividers, mInfos, mCandidatesPaneControl); 600 mPaneParams = new SuggestionsPaneParams( 601 mWords, mDividers, mInfos, mCandidatesPaneControl); 602 } 603 604 /** 605 * A connection back to the input method. 606 * @param listener 607 */ 608 public void setListener(Listener listener, View inputView) { 609 mListener = listener; 610 mKeyboardView = inputView.findViewById(R.id.keyboard_view); 611 mCandidatesPane = FrameLayoutCompatUtils.getPlacer( 612 (ViewGroup)inputView.findViewById(R.id.candidates_pane)); 613 mCandidatesPane.setOnClickListener(this); 614 mCandidatesPaneContainer = (ViewGroup)inputView.findViewById( 615 R.id.candidates_pane_container); 616 } 617 618 public void setSuggestions(SuggestedWords suggestions) { 619 if (suggestions == null) 620 return; 621 mSuggestions = suggestions; 622 mExpandCandidatesPane.setEnabled(false); 623 if (mShowingAutoCorrectionInverted) { 624 mHandler.postUpdateSuggestions(); 625 } else { 626 updateSuggestions(); 627 } 628 } 629 630 private void updateSuggestions() { 631 clear(); 632 closeCandidatesPane(); 633 if (mSuggestions.size() == 0) 634 return; 635 636 final int width = getWidth(); 637 final int countInStrip = mStripParams.layout( 638 mSuggestions, mCandidatesStrip, mCandidatesPane, width); 639 final int countInPane = mPaneParams.layout( 640 mSuggestions, mCandidatesPane, countInStrip, mStripParams.getTextColor(), width); 641 642 if (countInPane <= 0 && !DBG) { 643 mCandidatesPaneControl.setVisibility(GONE); 644 } else { 645 mCandidatesPaneControl.setVisibility(VISIBLE); 646 mExpandCandidatesPane.setVisibility(VISIBLE); 647 mExpandCandidatesPane.setEnabled(true); 648 } 649 } 650 651 private static CharSequence getDebugInfo(SuggestedWords suggestions, int pos) { 652 if (DBG) { 653 final SuggestedWordInfo wordInfo = suggestions.getInfo(pos); 654 if (wordInfo != null) { 655 final CharSequence debugInfo = wordInfo.getDebugString(); 656 if (!TextUtils.isEmpty(debugInfo)) { 657 return debugInfo; 658 } 659 } 660 } 661 return null; 662 } 663 664 private static void setLayoutWeight(View v, float weight, int height) { 665 final ViewGroup.LayoutParams lp = v.getLayoutParams(); 666 if (lp instanceof LinearLayout.LayoutParams) { 667 final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; 668 llp.weight = weight; 669 llp.width = 0; 670 llp.height = height; 671 } 672 } 673 674 private static void centeringCandidates(ViewGroup parent, View from, View to, int width, 675 int parentWidth) { 676 final int fromIndex = parent.indexOfChild(from); 677 final int toIndex = parent.indexOfChild(to); 678 final int offset = (parentWidth - width) / 2; 679 for (int index = fromIndex; index <= toIndex; index++) { 680 offsetMargin(parent.getChildAt(index), offset, 0); 681 } 682 } 683 684 private static void offsetMargin(View v, int dx, int dy) { 685 if (v == null) 686 return; 687 final ViewGroup.LayoutParams lp = v.getLayoutParams(); 688 if (lp instanceof ViewGroup.MarginLayoutParams) { 689 final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp; 690 mlp.setMargins(mlp.leftMargin + dx, mlp.topMargin + dy, 0, 0); 691 } 692 } 693 694 private static CharSequence getEllipsizedText(CharSequence text, int maxWidth, 695 TextPaint paint) { 696 paint.setTextScaleX(1.0f); 697 final int width = getTextWidth(text, paint); 698 final float scaleX = Math.min(maxWidth / (float)width, 1.0f); 699 if (scaleX >= MIN_TEXT_XSCALE) { 700 paint.setTextScaleX(scaleX); 701 return text; 702 } 703 704 // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To get 705 // squeezed and ellipsezed text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE). 706 final CharSequence ellipsized = TextUtils.ellipsize( 707 text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE); 708 paint.setTextScaleX(MIN_TEXT_XSCALE); 709 return ellipsized; 710 } 711 712 private static int getTextWidth(CharSequence text, TextPaint paint) { 713 if (TextUtils.isEmpty(text)) return 0; 714 final Typeface savedTypeface = paint.getTypeface(); 715 paint.setTypeface(getTextTypeface(text)); 716 final int len = text.length(); 717 final float[] widths = new float[len]; 718 final int count = paint.getTextWidths(text, 0, len, widths); 719 int width = 0; 720 for (int i = 0; i < count; i++) { 721 width += Math.round(widths[i] + 0.5f); 722 } 723 paint.setTypeface(savedTypeface); 724 return width; 725 } 726 727 private static Typeface getTextTypeface(CharSequence text) { 728 if (!(text instanceof SpannableString)) 729 return Typeface.DEFAULT; 730 731 final SpannableString ss = (SpannableString)text; 732 final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class); 733 if (styles.length == 0) 734 return Typeface.DEFAULT; 735 736 switch (styles[0].getStyle()) { 737 case Typeface.BOLD: return Typeface.DEFAULT_BOLD; 738 // TODO: BOLD_ITALIC, ITALIC case? 739 default: return Typeface.DEFAULT; 740 } 741 } 742 743 private void expandCandidatesPane() { 744 mExpandCandidatesPane.setVisibility(GONE); 745 mCloseCandidatesPane.setVisibility(VISIBLE); 746 mCandidatesPaneContainer.setMinimumHeight(mKeyboardView.getMeasuredHeight()); 747 mCandidatesPaneContainer.setVisibility(VISIBLE); 748 mKeyboardView.setVisibility(GONE); 749 } 750 751 private void closeCandidatesPane() { 752 mExpandCandidatesPane.setVisibility(VISIBLE); 753 mCloseCandidatesPane.setVisibility(GONE); 754 mCandidatesPaneContainer.setVisibility(GONE); 755 mKeyboardView.setVisibility(VISIBLE); 756 } 757 758 public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) { 759 final CharSequence inverted = mStripParams.getInvertedText(autoCorrectedWord); 760 if (inverted == null) 761 return; 762 final TextView tv = mWords.get(1); 763 tv.setText(inverted); 764 mShowingAutoCorrectionInverted = true; 765 } 766 767 public boolean isShowingAddToDictionaryHint() { 768 return mShowingAddToDictionary; 769 } 770 771 public void showAddToDictionaryHint(CharSequence word) { 772 mWordToSave.setText(word); 773 mShowingAddToDictionary = true; 774 mCandidatesStrip.setVisibility(GONE); 775 mCandidatesPaneControl.setVisibility(GONE); 776 mTouchToSave.setVisibility(VISIBLE); 777 } 778 779 public boolean dismissAddToDictionaryHint() { 780 if (!mShowingAddToDictionary) return false; 781 clear(); 782 return true; 783 } 784 785 public SuggestedWords getSuggestions() { 786 return mSuggestions; 787 } 788 789 public void clear() { 790 mShowingAddToDictionary = false; 791 mShowingAutoCorrectionInverted = false; 792 mTouchToSave.setVisibility(GONE); 793 mCandidatesStrip.setVisibility(VISIBLE); 794 mCandidatesStrip.removeAllViews(); 795 mCandidatesPane.removeAllViews(); 796 closeCandidatesPane(); 797 } 798 799 private void hidePreview() { 800 mPreviewPopup.dismiss(); 801 } 802 803 private void showPreview(int index, CharSequence word) { 804 if (TextUtils.isEmpty(word)) 805 return; 806 807 final TextView previewText = mPreviewText; 808 previewText.setTextColor(mStripParams.mColorTypedWord); 809 previewText.setText(word); 810 previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 811 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 812 View v = mWords.get(index); 813 final int[] offsetInWindow = new int[2]; 814 v.getLocationInWindow(offsetInWindow); 815 final int posX = offsetInWindow[0]; 816 final int posY = offsetInWindow[1] - previewText.getMeasuredHeight(); 817 final PopupWindow previewPopup = mPreviewPopup; 818 if (previewPopup.isShowing()) { 819 previewPopup.update(posX, posY, previewPopup.getWidth(), previewPopup.getHeight()); 820 } else { 821 previewPopup.showAtLocation(this, Gravity.NO_GRAVITY, posX, posY); 822 } 823 previewText.setVisibility(VISIBLE); 824 mHandler.postHidePreview(); 825 } 826 827 private void addToDictionary(CharSequence word) { 828 if (mListener.addWordToDictionary(word.toString())) { 829 showPreview(0, getContext().getString(R.string.added_word, word)); 830 } 831 } 832 833 @Override 834 public void onClick(View view) { 835 if (view == mWordToSave) { 836 addToDictionary(((TextView)view).getText()); 837 clear(); 838 return; 839 } 840 841 final Object tag = view.getTag(); 842 if (!(tag instanceof Integer)) 843 return; 844 final int index = (Integer) tag; 845 if (index >= mSuggestions.size()) 846 return; 847 848 final CharSequence word = mSuggestions.getWord(index); 849 mListener.pickSuggestionManually(index, word); 850 // Because some punctuation letters are not treated as word separator depending on locale, 851 // {@link #setSuggestions} might not be called and candidates pane left opened. 852 closeCandidatesPane(); 853 } 854 855 @Override 856 public void onDetachedFromWindow() { 857 super.onDetachedFromWindow(); 858 mHandler.cancelAllMessages(); 859 hidePreview(); 860 } 861} 862