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