1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.latin.suggestions; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Paint.Align; 27 import android.graphics.Rect; 28 import android.graphics.Typeface; 29 import android.graphics.drawable.BitmapDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.text.Spannable; 32 import android.text.SpannableString; 33 import android.text.Spanned; 34 import android.text.TextPaint; 35 import android.text.TextUtils; 36 import android.text.style.CharacterStyle; 37 import android.text.style.StyleSpan; 38 import android.text.style.UnderlineSpan; 39 import android.util.AttributeSet; 40 import android.view.Gravity; 41 import android.view.View; 42 import android.view.ViewGroup; 43 import android.widget.LinearLayout; 44 import android.widget.TextView; 45 46 import com.android.inputmethod.accessibility.AccessibilityUtils; 47 import com.android.inputmethod.annotations.UsedForTesting; 48 import com.android.inputmethod.latin.PunctuationSuggestions; 49 import com.android.inputmethod.latin.R; 50 import com.android.inputmethod.latin.SuggestedWords; 51 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 52 import com.android.inputmethod.latin.settings.Settings; 53 import com.android.inputmethod.latin.settings.SettingsValues; 54 import com.android.inputmethod.latin.utils.ResourceUtils; 55 import com.android.inputmethod.latin.utils.ViewLayoutUtils; 56 57 import java.util.ArrayList; 58 59 import javax.annotation.Nonnull; 60 import javax.annotation.Nullable; 61 62 final class SuggestionStripLayoutHelper { 63 private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3; 64 private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f; 65 private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2; 66 private static final int PUNCTUATIONS_IN_STRIP = 5; 67 private static final float MIN_TEXT_XSCALE = 0.70f; 68 69 public final int mPadding; 70 public final int mDividerWidth; 71 public final int mSuggestionsStripHeight; 72 private final int mSuggestionsCountInStrip; 73 public final int mMoreSuggestionsRowHeight; 74 private int mMaxMoreSuggestionsRow; 75 public final float mMinMoreSuggestionsWidth; 76 public final int mMoreSuggestionsBottomGap; 77 private boolean mMoreSuggestionsAvailable; 78 79 // The index of these {@link ArrayList} is the position in the suggestion strip. The indices 80 // increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. 81 // The position of the most important suggestion is in {@link #mCenterPositionInStrip} 82 private final ArrayList<TextView> mWordViews; 83 private final ArrayList<View> mDividerViews; 84 private final ArrayList<TextView> mDebugInfoViews; 85 86 private final int mColorValidTypedWord; 87 private final int mColorTypedWord; 88 private final int mColorAutoCorrect; 89 private final int mColorSuggested; 90 private final float mAlphaObsoleted; 91 private final float mCenterSuggestionWeight; 92 private final int mCenterPositionInStrip; 93 private final int mTypedWordPositionWhenAutocorrect; 94 private final Drawable mMoreSuggestionsHint; 95 private static final String MORE_SUGGESTIONS_HINT = "\u2026"; 96 97 private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); 98 private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); 99 100 private final int mSuggestionStripOptions; 101 // These constants are the flag values of 102 // {@link R.styleable#SuggestionStripView_suggestionStripOptions} attribute. 103 private static final int AUTO_CORRECT_BOLD = 0x01; 104 private static final int AUTO_CORRECT_UNDERLINE = 0x02; 105 private static final int VALID_TYPED_WORD_BOLD = 0x04; 106 107 public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs, 108 final int defStyle, final ArrayList<TextView> wordViews, 109 final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) { 110 mWordViews = wordViews; 111 mDividerViews = dividerViews; 112 mDebugInfoViews = debugInfoViews; 113 114 final TextView wordView = wordViews.get(0); 115 final View dividerView = dividerViews.get(0); 116 mPadding = wordView.getCompoundPaddingLeft() + wordView.getCompoundPaddingRight(); 117 dividerView.measure( 118 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 119 mDividerWidth = dividerView.getMeasuredWidth(); 120 121 final Resources res = wordView.getResources(); 122 mSuggestionsStripHeight = res.getDimensionPixelSize( 123 R.dimen.config_suggestions_strip_height); 124 125 final TypedArray a = context.obtainStyledAttributes(attrs, 126 R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView); 127 mSuggestionStripOptions = a.getInt( 128 R.styleable.SuggestionStripView_suggestionStripOptions, 0); 129 mAlphaObsoleted = ResourceUtils.getFraction(a, 130 R.styleable.SuggestionStripView_alphaObsoleted, 1.0f); 131 mColorValidTypedWord = a.getColor(R.styleable.SuggestionStripView_colorValidTypedWord, 0); 132 mColorTypedWord = a.getColor(R.styleable.SuggestionStripView_colorTypedWord, 0); 133 mColorAutoCorrect = a.getColor(R.styleable.SuggestionStripView_colorAutoCorrect, 0); 134 mColorSuggested = a.getColor(R.styleable.SuggestionStripView_colorSuggested, 0); 135 mSuggestionsCountInStrip = a.getInt( 136 R.styleable.SuggestionStripView_suggestionsCountInStrip, 137 DEFAULT_SUGGESTIONS_COUNT_IN_STRIP); 138 mCenterSuggestionWeight = ResourceUtils.getFraction(a, 139 R.styleable.SuggestionStripView_centerSuggestionPercentile, 140 DEFAULT_CENTER_SUGGESTION_PERCENTILE); 141 mMaxMoreSuggestionsRow = a.getInt( 142 R.styleable.SuggestionStripView_maxMoreSuggestionsRow, 143 DEFAULT_MAX_MORE_SUGGESTIONS_ROW); 144 mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a, 145 R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f); 146 a.recycle(); 147 148 mMoreSuggestionsHint = getMoreSuggestionsHint(res, 149 res.getDimension(R.dimen.config_more_suggestions_hint_text_size), 150 mColorAutoCorrect); 151 mCenterPositionInStrip = mSuggestionsCountInStrip / 2; 152 // Assuming there are at least three suggestions. Also, note that the suggestions are 153 // laid out according to script direction, so this is left of the center for LTR scripts 154 // and right of the center for RTL scripts. 155 mTypedWordPositionWhenAutocorrect = mCenterPositionInStrip - 1; 156 mMoreSuggestionsBottomGap = res.getDimensionPixelOffset( 157 R.dimen.config_more_suggestions_bottom_gap); 158 mMoreSuggestionsRowHeight = res.getDimensionPixelSize( 159 R.dimen.config_more_suggestions_row_height); 160 } 161 162 public int getMaxMoreSuggestionsRow() { 163 return mMaxMoreSuggestionsRow; 164 } 165 166 private int getMoreSuggestionsHeight() { 167 return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap; 168 } 169 170 public void setMoreSuggestionsHeight(final int remainingHeight) { 171 final int currentHeight = getMoreSuggestionsHeight(); 172 if (currentHeight <= remainingHeight) { 173 return; 174 } 175 176 mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap) 177 / mMoreSuggestionsRowHeight; 178 } 179 180 private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize, 181 final int color) { 182 final Paint paint = new Paint(); 183 paint.setAntiAlias(true); 184 paint.setTextAlign(Align.CENTER); 185 paint.setTextSize(textSize); 186 paint.setColor(color); 187 final Rect bounds = new Rect(); 188 paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds); 189 final int width = Math.round(bounds.width() + 0.5f); 190 final int height = Math.round(bounds.height() + 0.5f); 191 final Bitmap buffer = Bitmap.createBitmap(width, (height * 3 / 2), Bitmap.Config.ARGB_8888); 192 final Canvas canvas = new Canvas(buffer); 193 canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint); 194 BitmapDrawable bitmapDrawable = new BitmapDrawable(res, buffer); 195 bitmapDrawable.setTargetDensity(canvas); 196 return bitmapDrawable; 197 } 198 199 private CharSequence getStyledSuggestedWord(final SuggestedWords suggestedWords, 200 final int indexInSuggestedWords) { 201 if (indexInSuggestedWords >= suggestedWords.size()) { 202 return null; 203 } 204 final String word = suggestedWords.getLabel(indexInSuggestedWords); 205 // TODO: don't use the index to decide whether this is the auto-correction/typed word, as 206 // this is brittle 207 final boolean isAutoCorrection = suggestedWords.mWillAutoCorrect 208 && indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION; 209 final boolean isTypedWordValid = suggestedWords.mTypedWordValid 210 && indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD; 211 if (!isAutoCorrection && !isTypedWordValid) { 212 return word; 213 } 214 215 final Spannable spannedWord = new SpannableString(word); 216 final int options = mSuggestionStripOptions; 217 if ((isAutoCorrection && (options & AUTO_CORRECT_BOLD) != 0) 218 || (isTypedWordValid && (options & VALID_TYPED_WORD_BOLD) != 0)) { 219 addStyleSpan(spannedWord, BOLD_SPAN); 220 } 221 if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) { 222 addStyleSpan(spannedWord, UNDERLINE_SPAN); 223 } 224 return spannedWord; 225 } 226 227 /** 228 * Convert an index of {@link SuggestedWords} to position in the suggestion strip. 229 * @param indexInSuggestedWords the index of {@link SuggestedWords}. 230 * @param suggestedWords the suggested words list 231 * @return Non-negative integer of the position in the suggestion strip. 232 * Negative integer if the word of the index shouldn't be shown on the suggestion strip. 233 */ 234 private int getPositionInSuggestionStrip(final int indexInSuggestedWords, 235 final SuggestedWords suggestedWords) { 236 final SettingsValues settingsValues = Settings.getInstance().getCurrent(); 237 final boolean shouldOmitTypedWord = shouldOmitTypedWord(suggestedWords.mInputStyle, 238 settingsValues.mGestureFloatingPreviewTextEnabled, 239 settingsValues.mShouldShowLxxSuggestionUi); 240 return getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords.mWillAutoCorrect, 241 settingsValues.mShouldShowLxxSuggestionUi && shouldOmitTypedWord, 242 mCenterPositionInStrip, mTypedWordPositionWhenAutocorrect); 243 } 244 245 @UsedForTesting 246 static boolean shouldOmitTypedWord(final int inputStyle, 247 final boolean gestureFloatingPreviewTextEnabled, 248 final boolean shouldShowUiToAcceptTypedWord) { 249 final boolean omitTypedWord = (inputStyle == SuggestedWords.INPUT_STYLE_TYPING) 250 || (inputStyle == SuggestedWords.INPUT_STYLE_TAIL_BATCH) 251 || (inputStyle == SuggestedWords.INPUT_STYLE_UPDATE_BATCH 252 && gestureFloatingPreviewTextEnabled); 253 return shouldShowUiToAcceptTypedWord && omitTypedWord; 254 } 255 256 @UsedForTesting 257 static int getPositionInSuggestionStrip(final int indexInSuggestedWords, 258 final boolean willAutoCorrect, final boolean omitTypedWord, 259 final int centerPositionInStrip, final int typedWordPositionWhenAutoCorrect) { 260 if (omitTypedWord) { 261 if (indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD) { 262 // Ignore. 263 return -1; 264 } 265 if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION) { 266 // Center in the suggestion strip. 267 return centerPositionInStrip; 268 } 269 // If neither of those, the order in the suggestion strip is left of the center first 270 // then right of the center, to both edges of the suggestion strip. 271 // For example, center-1, center+1, center-2, center+2, and so on. 272 final int n = indexInSuggestedWords; 273 final int offsetFromCenter = (n % 2) == 0 ? -(n / 2) : (n / 2); 274 final int positionInSuggestionStrip = centerPositionInStrip + offsetFromCenter; 275 return positionInSuggestionStrip; 276 } 277 final int indexToDisplayMostImportantSuggestion; 278 final int indexToDisplaySecondMostImportantSuggestion; 279 if (willAutoCorrect) { 280 indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; 281 indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; 282 } else { 283 indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; 284 indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; 285 } 286 if (indexInSuggestedWords == indexToDisplayMostImportantSuggestion) { 287 // Center in the suggestion strip. 288 return centerPositionInStrip; 289 } 290 if (indexInSuggestedWords == indexToDisplaySecondMostImportantSuggestion) { 291 // Center-1. 292 return typedWordPositionWhenAutoCorrect; 293 } 294 // If neither of those, the order in the suggestion strip is right of the center first 295 // then left of the center, to both edges of the suggestion strip. 296 // For example, Center+1, center-2, center+2, center-3, and so on. 297 final int n = indexInSuggestedWords + 1; 298 final int offsetFromCenter = (n % 2) == 0 ? -(n / 2) : (n / 2); 299 final int positionInSuggestionStrip = centerPositionInStrip + offsetFromCenter; 300 return positionInSuggestionStrip; 301 } 302 303 private int getSuggestionTextColor(final SuggestedWords suggestedWords, 304 final int indexInSuggestedWords) { 305 // Use identity for strings, not #equals : it's the typed word if it's the same object 306 final boolean isTypedWord = suggestedWords.getInfo(indexInSuggestedWords).isKindOf( 307 SuggestedWordInfo.KIND_TYPED); 308 309 final int color; 310 if (indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION 311 && suggestedWords.mWillAutoCorrect) { 312 color = mColorAutoCorrect; 313 } else if (isTypedWord && suggestedWords.mTypedWordValid) { 314 color = mColorValidTypedWord; 315 } else if (isTypedWord) { 316 color = mColorTypedWord; 317 } else { 318 color = mColorSuggested; 319 } 320 if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) { 321 return applyAlpha(color, mAlphaObsoleted); 322 } 323 return color; 324 } 325 326 private static int applyAlpha(final int color, final float alpha) { 327 final int newAlpha = (int)(Color.alpha(color) * alpha); 328 return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); 329 } 330 331 private static void addDivider(final ViewGroup stripView, final View dividerView) { 332 stripView.addView(dividerView); 333 final LinearLayout.LayoutParams params = 334 (LinearLayout.LayoutParams)dividerView.getLayoutParams(); 335 params.gravity = Gravity.CENTER; 336 } 337 338 /** 339 * Layout suggestions to the suggestions strip. And returns the start index of more 340 * suggestions. 341 * 342 * @param suggestedWords suggestions to be shown in the suggestions strip. 343 * @param stripView the suggestions strip view. 344 * @param placerView the view where the debug info will be placed. 345 * @return the start index of more suggestions. 346 */ 347 public int layoutAndReturnStartIndexOfMoreSuggestions( 348 final Context context, 349 final SuggestedWords suggestedWords, 350 final ViewGroup stripView, 351 final ViewGroup placerView) { 352 if (suggestedWords.isPunctuationSuggestions()) { 353 return layoutPunctuationsAndReturnStartIndexOfMoreSuggestions( 354 (PunctuationSuggestions)suggestedWords, stripView); 355 } 356 357 final int wordCountToShow = suggestedWords.getWordCountToShow( 358 Settings.getInstance().getCurrent().mShouldShowLxxSuggestionUi); 359 final int startIndexOfMoreSuggestions = setupWordViewsAndReturnStartIndexOfMoreSuggestions( 360 suggestedWords, mSuggestionsCountInStrip); 361 final TextView centerWordView = mWordViews.get(mCenterPositionInStrip); 362 final int stripWidth = stripView.getWidth(); 363 final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth); 364 if (wordCountToShow == 1 || getTextScaleX(centerWordView.getText(), centerWidth, 365 centerWordView.getPaint()) < MIN_TEXT_XSCALE) { 366 // Layout only the most relevant suggested word at the center of the suggestion strip 367 // by consolidating all slots in the strip. 368 final int countInStrip = 1; 369 mMoreSuggestionsAvailable = (wordCountToShow > countInStrip); 370 layoutWord(context, mCenterPositionInStrip, stripWidth - mPadding); 371 stripView.addView(centerWordView); 372 setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT); 373 if (SuggestionStripView.DBG) { 374 layoutDebugInfo(mCenterPositionInStrip, placerView, stripWidth); 375 } 376 final Integer lastIndex = (Integer)centerWordView.getTag(); 377 return (lastIndex == null ? 0 : lastIndex) + 1; 378 } 379 380 final int countInStrip = mSuggestionsCountInStrip; 381 mMoreSuggestionsAvailable = (wordCountToShow > countInStrip); 382 @SuppressWarnings("unused") 383 int x = 0; 384 for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { 385 if (positionInStrip != 0) { 386 final View divider = mDividerViews.get(positionInStrip); 387 // Add divider if this isn't the left most suggestion in suggestions strip. 388 addDivider(stripView, divider); 389 x += divider.getMeasuredWidth(); 390 } 391 392 final int width = getSuggestionWidth(positionInStrip, stripWidth); 393 final TextView wordView = layoutWord(context, positionInStrip, width); 394 stripView.addView(wordView); 395 setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), 396 ViewGroup.LayoutParams.MATCH_PARENT); 397 x += wordView.getMeasuredWidth(); 398 399 if (SuggestionStripView.DBG) { 400 layoutDebugInfo(positionInStrip, placerView, x); 401 } 402 } 403 return startIndexOfMoreSuggestions; 404 } 405 406 /** 407 * Format appropriately the suggested word in {@link #mWordViews} specified by 408 * <code>positionInStrip</code>. When the suggested word doesn't exist, the corresponding 409 * {@link TextView} will be disabled and never respond to user interaction. The suggested word 410 * may be shrunk or ellipsized to fit in the specified width. 411 * 412 * The <code>positionInStrip</code> argument is the index in the suggestion strip. The indices 413 * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. 414 * The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This 415 * usually doesn't match the index in <code>suggedtedWords</code> -- see 416 * {@link #getPositionInSuggestionStrip(int,SuggestedWords)}. 417 * 418 * @param positionInStrip the position in the suggestion strip. 419 * @param width the maximum width for layout in pixels. 420 * @return the {@link TextView} containing the suggested word appropriately formatted. 421 */ 422 private TextView layoutWord(final Context context, final int positionInStrip, final int width) { 423 final TextView wordView = mWordViews.get(positionInStrip); 424 final CharSequence word = wordView.getText(); 425 if (positionInStrip == mCenterPositionInStrip && mMoreSuggestionsAvailable) { 426 // TODO: This "more suggestions hint" should have a nicely designed icon. 427 wordView.setCompoundDrawablesWithIntrinsicBounds( 428 null, null, null, mMoreSuggestionsHint); 429 // HACK: Align with other TextViews that have no compound drawables. 430 wordView.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight()); 431 } else { 432 wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); 433 } 434 // {@link StyleSpan} in a content description may cause an issue of TTS/TalkBack. 435 // Use a simple {@link String} to avoid the issue. 436 wordView.setContentDescription( 437 TextUtils.isEmpty(word) 438 ? context.getResources().getString(R.string.spoken_empty_suggestion) 439 : word.toString()); 440 final CharSequence text = getEllipsizedTextWithSettingScaleX( 441 word, width, wordView.getPaint()); 442 final float scaleX = wordView.getTextScaleX(); 443 wordView.setText(text); // TextView.setText() resets text scale x to 1.0. 444 wordView.setTextScaleX(scaleX); 445 // A <code>wordView</code> should be disabled when <code>word</code> is empty in order to 446 // make it unclickable. 447 // With accessibility touch exploration on, <code>wordView</code> should be enabled even 448 // when it is empty to avoid announcing as "disabled". 449 wordView.setEnabled(!TextUtils.isEmpty(word) 450 || AccessibilityUtils.getInstance().isTouchExplorationEnabled()); 451 return wordView; 452 } 453 454 private void layoutDebugInfo(final int positionInStrip, final ViewGroup placerView, 455 final int x) { 456 final TextView debugInfoView = mDebugInfoViews.get(positionInStrip); 457 final CharSequence debugInfo = debugInfoView.getText(); 458 if (debugInfo == null) { 459 return; 460 } 461 placerView.addView(debugInfoView); 462 debugInfoView.measure( 463 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 464 final int infoWidth = debugInfoView.getMeasuredWidth(); 465 final int y = debugInfoView.getMeasuredHeight(); 466 ViewLayoutUtils.placeViewAt( 467 debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight()); 468 } 469 470 private int getSuggestionWidth(final int positionInStrip, final int maxWidth) { 471 final int paddings = mPadding * mSuggestionsCountInStrip; 472 final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1); 473 final int availableWidth = maxWidth - paddings - dividers; 474 return (int)(availableWidth * getSuggestionWeight(positionInStrip)); 475 } 476 477 private float getSuggestionWeight(final int positionInStrip) { 478 if (positionInStrip == mCenterPositionInStrip) { 479 return mCenterSuggestionWeight; 480 } 481 // TODO: Revisit this for cases of 5 or more suggestions 482 return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1); 483 } 484 485 private int setupWordViewsAndReturnStartIndexOfMoreSuggestions( 486 final SuggestedWords suggestedWords, final int maxSuggestionInStrip) { 487 // Clear all suggestions first 488 for (int positionInStrip = 0; positionInStrip < maxSuggestionInStrip; ++positionInStrip) { 489 final TextView wordView = mWordViews.get(positionInStrip); 490 wordView.setText(null); 491 wordView.setTag(null); 492 // Make this inactive for touches in {@link #layoutWord(int,int)}. 493 if (SuggestionStripView.DBG) { 494 mDebugInfoViews.get(positionInStrip).setText(null); 495 } 496 } 497 int count = 0; 498 int indexInSuggestedWords; 499 for (indexInSuggestedWords = 0; indexInSuggestedWords < suggestedWords.size() 500 && count < maxSuggestionInStrip; indexInSuggestedWords++) { 501 final int positionInStrip = 502 getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords); 503 if (positionInStrip < 0) { 504 continue; 505 } 506 final TextView wordView = mWordViews.get(positionInStrip); 507 // {@link TextView#getTag()} is used to get the index in suggestedWords at 508 // {@link SuggestionStripView#onClick(View)}. 509 wordView.setTag(indexInSuggestedWords); 510 wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords)); 511 wordView.setTextColor(getSuggestionTextColor(suggestedWords, indexInSuggestedWords)); 512 if (SuggestionStripView.DBG) { 513 mDebugInfoViews.get(positionInStrip).setText( 514 suggestedWords.getDebugString(indexInSuggestedWords)); 515 } 516 count++; 517 } 518 return indexInSuggestedWords; 519 } 520 521 private int layoutPunctuationsAndReturnStartIndexOfMoreSuggestions( 522 final PunctuationSuggestions punctuationSuggestions, final ViewGroup stripView) { 523 final int countInStrip = Math.min(punctuationSuggestions.size(), PUNCTUATIONS_IN_STRIP); 524 for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { 525 if (positionInStrip != 0) { 526 // Add divider if this isn't the left most suggestion in suggestions strip. 527 addDivider(stripView, mDividerViews.get(positionInStrip)); 528 } 529 530 final TextView wordView = mWordViews.get(positionInStrip); 531 final String punctuation = punctuationSuggestions.getLabel(positionInStrip); 532 // {@link TextView#getTag()} is used to get the index in suggestedWords at 533 // {@link SuggestionStripView#onClick(View)}. 534 wordView.setTag(positionInStrip); 535 wordView.setText(punctuation); 536 wordView.setContentDescription(punctuation); 537 wordView.setTextScaleX(1.0f); 538 wordView.setCompoundDrawables(null, null, null, null); 539 wordView.setTextColor(mColorAutoCorrect); 540 stripView.addView(wordView); 541 setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight); 542 } 543 mMoreSuggestionsAvailable = (punctuationSuggestions.size() > countInStrip); 544 return countInStrip; 545 } 546 547 public void layoutImportantNotice(final View importantNoticeStrip, 548 final String importantNoticeTitle) { 549 final TextView titleView = (TextView)importantNoticeStrip.findViewById( 550 R.id.important_notice_title); 551 final int width = titleView.getWidth() - titleView.getPaddingLeft() 552 - titleView.getPaddingRight(); 553 titleView.setTextColor(mColorAutoCorrect); 554 titleView.setText(importantNoticeTitle); // TextView.setText() resets text scale x to 1.0. 555 final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint()); 556 titleView.setTextScaleX(titleScaleX); 557 } 558 559 static void setLayoutWeight(final View v, final float weight, final int height) { 560 final ViewGroup.LayoutParams lp = v.getLayoutParams(); 561 if (lp instanceof LinearLayout.LayoutParams) { 562 final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; 563 llp.weight = weight; 564 llp.width = 0; 565 llp.height = height; 566 } 567 } 568 569 private static float getTextScaleX(@Nullable final CharSequence text, final int maxWidth, 570 final TextPaint paint) { 571 paint.setTextScaleX(1.0f); 572 final int width = getTextWidth(text, paint); 573 if (width <= maxWidth || maxWidth <= 0) { 574 return 1.0f; 575 } 576 return maxWidth / (float) width; 577 } 578 579 @Nullable 580 private static CharSequence getEllipsizedTextWithSettingScaleX( 581 @Nullable final CharSequence text, final int maxWidth, @Nonnull final TextPaint paint) { 582 if (text == null) { 583 return null; 584 } 585 final float scaleX = getTextScaleX(text, maxWidth, paint); 586 if (scaleX >= MIN_TEXT_XSCALE) { 587 paint.setTextScaleX(scaleX); 588 return text; 589 } 590 591 // <code>text</code> must be ellipsized with minimum text scale x. 592 paint.setTextScaleX(MIN_TEXT_XSCALE); 593 final boolean hasBoldStyle = hasStyleSpan(text, BOLD_SPAN); 594 final boolean hasUnderlineStyle = hasStyleSpan(text, UNDERLINE_SPAN); 595 // TextUtils.ellipsize erases any span object existed after ellipsized point. 596 // We have to restore these spans afterward. 597 final CharSequence ellipsizedText = TextUtils.ellipsize( 598 text, paint, maxWidth, TextUtils.TruncateAt.MIDDLE); 599 if (!hasBoldStyle && !hasUnderlineStyle) { 600 return ellipsizedText; 601 } 602 final Spannable spannableText = (ellipsizedText instanceof Spannable) 603 ? (Spannable)ellipsizedText : new SpannableString(ellipsizedText); 604 if (hasBoldStyle) { 605 addStyleSpan(spannableText, BOLD_SPAN); 606 } 607 if (hasUnderlineStyle) { 608 addStyleSpan(spannableText, UNDERLINE_SPAN); 609 } 610 return spannableText; 611 } 612 613 private static boolean hasStyleSpan(@Nullable final CharSequence text, 614 final CharacterStyle style) { 615 if (text instanceof Spanned) { 616 return ((Spanned)text).getSpanStart(style) >= 0; 617 } 618 return false; 619 } 620 621 private static void addStyleSpan(@Nonnull final Spannable text, final CharacterStyle style) { 622 text.removeSpan(style); 623 text.setSpan(style, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 624 } 625 626 private static int getTextWidth(@Nullable final CharSequence text, final TextPaint paint) { 627 if (TextUtils.isEmpty(text)) { 628 return 0; 629 } 630 final int length = text.length(); 631 final float[] widths = new float[length]; 632 final int count; 633 final Typeface savedTypeface = paint.getTypeface(); 634 try { 635 paint.setTypeface(getTextTypeface(text)); 636 count = paint.getTextWidths(text, 0, length, widths); 637 } finally { 638 paint.setTypeface(savedTypeface); 639 } 640 int width = 0; 641 for (int i = 0; i < count; i++) { 642 width += Math.round(widths[i] + 0.5f); 643 } 644 return width; 645 } 646 647 private static Typeface getTextTypeface(@Nullable final CharSequence text) { 648 return hasStyleSpan(text, BOLD_SPAN) ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT; 649 } 650 } 651