SuggestionStripLayoutHelper.java revision 14e908c3486ae5996e66625e959ff45c5f3740ef
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 17package com.android.inputmethod.latin.suggestions; 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.support.v4.view.ViewCompat; 32import android.text.Spannable; 33import android.text.SpannableString; 34import android.text.Spanned; 35import android.text.TextPaint; 36import android.text.TextUtils; 37import android.text.style.CharacterStyle; 38import android.text.style.StyleSpan; 39import android.text.style.UnderlineSpan; 40import android.util.AttributeSet; 41import android.view.Gravity; 42import android.view.View; 43import android.view.ViewGroup; 44import android.widget.LinearLayout; 45import android.widget.TextView; 46 47import com.android.inputmethod.latin.LatinImeLogger; 48import com.android.inputmethod.latin.PunctuationSuggestions; 49import com.android.inputmethod.latin.R; 50import com.android.inputmethod.latin.SuggestedWords; 51import com.android.inputmethod.latin.utils.AutoCorrectionUtils; 52import com.android.inputmethod.latin.utils.ResourceUtils; 53import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 54import com.android.inputmethod.latin.utils.ViewLayoutUtils; 55 56import java.util.ArrayList; 57 58final class SuggestionStripLayoutHelper { 59 private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3; 60 private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f; 61 private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2; 62 private static final int PUNCTUATIONS_IN_STRIP = 5; 63 private static final float MIN_TEXT_XSCALE = 0.70f; 64 65 public final int mPadding; 66 public final int mDividerWidth; 67 public final int mSuggestionsStripHeight; 68 private final int mSuggestionsCountInStrip; 69 public final int mMoreSuggestionsRowHeight; 70 private int mMaxMoreSuggestionsRow; 71 public final float mMinMoreSuggestionsWidth; 72 public final int mMoreSuggestionsBottomGap; 73 public boolean mMoreSuggestionsAvailable; 74 75 // The index of these {@link ArrayList} is the position in the suggestion strip. The indices 76 // increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. 77 // The position of the most important suggestion is in {@link #mCenterPositionInStrip} 78 private final ArrayList<TextView> mWordViews; 79 private final ArrayList<View> mDividerViews; 80 private final ArrayList<TextView> mDebugInfoViews; 81 82 private final int mColorValidTypedWord; 83 private final int mColorTypedWord; 84 private final int mColorAutoCorrect; 85 private final int mColorSuggested; 86 private final float mAlphaObsoleted; 87 private final float mCenterSuggestionWeight; 88 private final int mCenterPositionInStrip; 89 private final int mTypedWordPositionWhenAutocorrect; 90 private final Drawable mMoreSuggestionsHint; 91 private static final String MORE_SUGGESTIONS_HINT = "\u2026"; 92 private static final String LEFTWARDS_ARROW = "\u2190"; 93 private static final String RIGHTWARDS_ARROW = "\u2192"; 94 95 private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); 96 private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); 97 98 private final int mSuggestionStripOptions; 99 // These constants are the flag values of 100 // {@link R.styleable#SuggestionStripView_suggestionStripOptions} attribute. 101 private static final int AUTO_CORRECT_BOLD = 0x01; 102 private static final int AUTO_CORRECT_UNDERLINE = 0x02; 103 private static final int VALID_TYPED_WORD_BOLD = 0x04; 104 105 public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs, 106 final int defStyle, final ArrayList<TextView> wordViews, 107 final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) { 108 mWordViews = wordViews; 109 mDividerViews = dividerViews; 110 mDebugInfoViews = debugInfoViews; 111 112 final TextView wordView = wordViews.get(0); 113 final View dividerView = dividerViews.get(0); 114 mPadding = wordView.getCompoundPaddingLeft() + wordView.getCompoundPaddingRight(); 115 dividerView.measure( 116 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 117 mDividerWidth = dividerView.getMeasuredWidth(); 118 119 final Resources res = wordView.getResources(); 120 mSuggestionsStripHeight = res.getDimensionPixelSize( 121 R.dimen.config_suggestions_strip_height); 122 123 final TypedArray a = context.obtainStyledAttributes(attrs, 124 R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripView); 125 mSuggestionStripOptions = a.getInt( 126 R.styleable.SuggestionStripView_suggestionStripOptions, 0); 127 mAlphaObsoleted = ResourceUtils.getFraction(a, 128 R.styleable.SuggestionStripView_alphaObsoleted, 1.0f); 129 mColorValidTypedWord = a.getColor(R.styleable.SuggestionStripView_colorValidTypedWord, 0); 130 mColorTypedWord = a.getColor(R.styleable.SuggestionStripView_colorTypedWord, 0); 131 mColorAutoCorrect = a.getColor(R.styleable.SuggestionStripView_colorAutoCorrect, 0); 132 mColorSuggested = a.getColor(R.styleable.SuggestionStripView_colorSuggested, 0); 133 mSuggestionsCountInStrip = a.getInt( 134 R.styleable.SuggestionStripView_suggestionsCountInStrip, 135 DEFAULT_SUGGESTIONS_COUNT_IN_STRIP); 136 mCenterSuggestionWeight = ResourceUtils.getFraction(a, 137 R.styleable.SuggestionStripView_centerSuggestionPercentile, 138 DEFAULT_CENTER_SUGGESTION_PERCENTILE); 139 mMaxMoreSuggestionsRow = a.getInt( 140 R.styleable.SuggestionStripView_maxMoreSuggestionsRow, 141 DEFAULT_MAX_MORE_SUGGESTIONS_ROW); 142 mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a, 143 R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f); 144 a.recycle(); 145 146 mMoreSuggestionsHint = getMoreSuggestionsHint(res, 147 res.getDimension(R.dimen.config_more_suggestions_hint_text_size), 148 mColorAutoCorrect); 149 mCenterPositionInStrip = mSuggestionsCountInStrip / 2; 150 // Assuming there are at least three suggestions. Also, note that the suggestions are 151 // laid out according to script direction, so this is left of the center for LTR scripts 152 // and right of the center for RTL scripts. 153 mTypedWordPositionWhenAutocorrect = mCenterPositionInStrip - 1; 154 mMoreSuggestionsBottomGap = res.getDimensionPixelOffset( 155 R.dimen.config_more_suggestions_bottom_gap); 156 mMoreSuggestionsRowHeight = res.getDimensionPixelSize( 157 R.dimen.config_more_suggestions_row_height); 158 } 159 160 public int getMaxMoreSuggestionsRow() { 161 return mMaxMoreSuggestionsRow; 162 } 163 164 private int getMoreSuggestionsHeight() { 165 return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap; 166 } 167 168 public int setMoreSuggestionsHeight(final int remainingHeight) { 169 final int currentHeight = getMoreSuggestionsHeight(); 170 if (currentHeight <= remainingHeight) { 171 return currentHeight; 172 } 173 174 mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap) 175 / mMoreSuggestionsRowHeight; 176 final int newHeight = getMoreSuggestionsHeight(); 177 return newHeight; 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 return new BitmapDrawable(res, buffer); 195 } 196 197 private CharSequence getStyledSuggestedWord(final SuggestedWords suggestedWords, 198 final int indexInSuggestedWords) { 199 if (indexInSuggestedWords >= suggestedWords.size()) { 200 return null; 201 } 202 final String word = suggestedWords.getLabel(indexInSuggestedWords); 203 // TODO: don't use the index to decide whether this is the auto-correction/typed word, as 204 // this is brittle 205 final boolean isAutoCorrection = suggestedWords.mWillAutoCorrect 206 && indexInSuggestedWords == SuggestedWords.INDEX_OF_AUTO_CORRECTION; 207 final boolean isTypedWordValid = suggestedWords.mTypedWordValid 208 && indexInSuggestedWords == SuggestedWords.INDEX_OF_TYPED_WORD; 209 if (!isAutoCorrection && !isTypedWordValid) { 210 return word; 211 } 212 213 final int len = word.length(); 214 final Spannable spannedWord = new SpannableString(word); 215 final int options = mSuggestionStripOptions; 216 if ((isAutoCorrection && (options & AUTO_CORRECT_BOLD) != 0) 217 || (isTypedWordValid && (options & VALID_TYPED_WORD_BOLD) != 0)) { 218 spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 219 } 220 if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) { 221 spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 222 } 223 return spannedWord; 224 } 225 226 private int getPositionInSuggestionStrip(final int indexInSuggestedWords, 227 final SuggestedWords suggestedWords) { 228 final int indexToDisplayMostImportantSuggestion; 229 final int indexToDisplaySecondMostImportantSuggestion; 230 if (suggestedWords.mWillAutoCorrect) { 231 indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; 232 indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; 233 } else { 234 indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; 235 indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; 236 } 237 if (indexInSuggestedWords == indexToDisplayMostImportantSuggestion) { 238 return mCenterPositionInStrip; 239 } 240 if (indexInSuggestedWords == indexToDisplaySecondMostImportantSuggestion) { 241 return mTypedWordPositionWhenAutocorrect; 242 } 243 // If neither of those, the order in the suggestion strip is the same as in SuggestedWords. 244 return indexInSuggestedWords; 245 } 246 247 private int getSuggestionTextColor(final SuggestedWords suggestedWords, 248 final int indexInSuggestedWords) { 249 final int positionInStrip = 250 getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords); 251 // Use identity for strings, not #equals : it's the typed word if it's the same object 252 final boolean isTypedWord = 253 suggestedWords.getWord(indexInSuggestedWords) == suggestedWords.mTypedWord; 254 255 final int color; 256 if (positionInStrip == mCenterPositionInStrip && suggestedWords.mWillAutoCorrect) { 257 color = mColorAutoCorrect; 258 } else if (isTypedWord && suggestedWords.mTypedWordValid) { 259 color = mColorValidTypedWord; 260 } else if (isTypedWord) { 261 color = mColorTypedWord; 262 } else { 263 color = mColorSuggested; 264 } 265 if (LatinImeLogger.sDBG && suggestedWords.size() > 1) { 266 // If we auto-correct, then the autocorrection is in slot 0 and the typed word 267 // is in slot 1. 268 if (positionInStrip == mCenterPositionInStrip 269 && AutoCorrectionUtils.shouldBlockAutoCorrectionBySafetyNet( 270 suggestedWords.getLabel(SuggestedWords.INDEX_OF_AUTO_CORRECTION), 271 suggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD))) { 272 return 0xFFFF0000; 273 } 274 } 275 276 if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) { 277 return applyAlpha(color, mAlphaObsoleted); 278 } 279 return color; 280 } 281 282 private static int applyAlpha(final int color, final float alpha) { 283 final int newAlpha = (int)(Color.alpha(color) * alpha); 284 return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); 285 } 286 287 private static void addDivider(final ViewGroup stripView, final View dividerView) { 288 stripView.addView(dividerView); 289 final LinearLayout.LayoutParams params = 290 (LinearLayout.LayoutParams)dividerView.getLayoutParams(); 291 params.gravity = Gravity.CENTER; 292 } 293 294 /** 295 * Layout suggestions to the suggestions strip. And returns the number of suggestions displayed 296 * in the suggestions strip. 297 * 298 * @param suggestedWords suggestions to be shown in the suggestions strip. 299 * @param stripView the suggestions strip view. 300 * @param placerView the view where the debug info will be placed. 301 * @return the number of suggestions displayed in the suggestions strip 302 */ 303 public int layoutAndReturnSuggestionCountInStrip(final SuggestedWords suggestedWords, 304 final ViewGroup stripView, final ViewGroup placerView) { 305 if (suggestedWords.isPunctuationSuggestions()) { 306 return layoutPunctuationSuggestionsAndReturnSuggestionCountInStrip( 307 (PunctuationSuggestions)suggestedWords, stripView); 308 } 309 310 setupWordViewsTextAndColor(suggestedWords, mSuggestionsCountInStrip); 311 final TextView centerWordView = mWordViews.get(mCenterPositionInStrip); 312 final int availableStripWidth = placerView.getWidth() 313 - placerView.getPaddingRight() - placerView.getPaddingLeft(); 314 final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, availableStripWidth); 315 final int countInStrip; 316 if (suggestedWords.size() == 1 || getTextScaleX(centerWordView.getText(), centerWidth, 317 centerWordView.getPaint()) < MIN_TEXT_XSCALE) { 318 // Layout only the most relevant suggested word at the center of the suggestion strip 319 // by consolidating all slots in the strip. 320 countInStrip = 1; 321 mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); 322 layoutWord(mCenterPositionInStrip, availableStripWidth - mPadding); 323 stripView.addView(centerWordView); 324 setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT); 325 if (SuggestionStripView.DBG) { 326 layoutDebugInfo(mCenterPositionInStrip, placerView, availableStripWidth); 327 } 328 } else { 329 countInStrip = mSuggestionsCountInStrip; 330 mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); 331 int x = 0; 332 for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { 333 if (positionInStrip != 0) { 334 final View divider = mDividerViews.get(positionInStrip); 335 // Add divider if this isn't the left most suggestion in suggestions strip. 336 addDivider(stripView, divider); 337 x += divider.getMeasuredWidth(); 338 } 339 340 final int width = getSuggestionWidth(positionInStrip, availableStripWidth); 341 final TextView wordView = layoutWord(positionInStrip, width); 342 stripView.addView(wordView); 343 setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), 344 ViewGroup.LayoutParams.MATCH_PARENT); 345 x += wordView.getMeasuredWidth(); 346 347 if (SuggestionStripView.DBG) { 348 layoutDebugInfo(positionInStrip, placerView, x); 349 } 350 } 351 } 352 return countInStrip; 353 } 354 355 /** 356 * Format appropriately the suggested word in {@link #mWordViews} specified by 357 * <code>positionInStrip</code>. When the suggested word doesn't exist, the corresponding 358 * {@link TextView} will be disabled and never respond to user interaction. The suggested word 359 * may be shrunk or ellipsized to fit in the specified width. 360 * 361 * The <code>positionInStrip</code> argument is the index in the suggestion strip. The indices 362 * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. 363 * The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This 364 * usually doesn't match the index in <code>suggedtedWords</code> -- see 365 * {@link #getPositionInSuggestionStrip(int,SuggestedWords)}. 366 * 367 * @param positionInStrip the position in the suggestion strip. 368 * @param width the maximum width for layout in pixels. 369 * @return the {@link TextView} containing the suggested word appropriately formatted. 370 */ 371 private TextView layoutWord(final int positionInStrip, final int width) { 372 final TextView wordView = mWordViews.get(positionInStrip); 373 final CharSequence word = wordView.getText(); 374 if (positionInStrip == mCenterPositionInStrip && mMoreSuggestionsAvailable) { 375 // TODO: This "more suggestions hint" should have a nicely designed icon. 376 wordView.setCompoundDrawablesWithIntrinsicBounds( 377 null, null, null, mMoreSuggestionsHint); 378 // HACK: Align with other TextViews that have no compound drawables. 379 wordView.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight()); 380 } else { 381 wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); 382 } 383 384 // Disable this suggestion if the suggestion is null or empty. 385 wordView.setEnabled(!TextUtils.isEmpty(word)); 386 final CharSequence text = getEllipsizedText(word, width, wordView.getPaint()); 387 final float scaleX = getTextScaleX(word, width, wordView.getPaint()); 388 wordView.setText(text); // TextView.setText() resets text scale x to 1.0. 389 wordView.setTextScaleX(Math.max(scaleX, MIN_TEXT_XSCALE)); 390 return wordView; 391 } 392 393 private void layoutDebugInfo(final int positionInStrip, final ViewGroup placerView, 394 final int x) { 395 final TextView debugInfoView = mDebugInfoViews.get(positionInStrip); 396 final CharSequence debugInfo = debugInfoView.getText(); 397 if (debugInfo == null) { 398 return; 399 } 400 placerView.addView(debugInfoView); 401 debugInfoView.measure( 402 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 403 final int infoWidth = debugInfoView.getMeasuredWidth(); 404 final int y = debugInfoView.getMeasuredHeight(); 405 ViewLayoutUtils.placeViewAt( 406 debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight()); 407 } 408 409 private int getSuggestionWidth(final int positionInStrip, final int maxWidth) { 410 final int paddings = mPadding * mSuggestionsCountInStrip; 411 final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1); 412 final int availableWidth = maxWidth - paddings - dividers; 413 return (int)(availableWidth * getSuggestionWeight(positionInStrip)); 414 } 415 416 private float getSuggestionWeight(final int positionInStrip) { 417 if (positionInStrip == mCenterPositionInStrip) { 418 return mCenterSuggestionWeight; 419 } 420 // TODO: Revisit this for cases of 5 or more suggestions 421 return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1); 422 } 423 424 private void setupWordViewsTextAndColor(final SuggestedWords suggestedWords, 425 final int countInStrip) { 426 // Clear all suggestions first 427 for (int positionInStrip = 0; positionInStrip < countInStrip; ++positionInStrip) { 428 mWordViews.get(positionInStrip).setText(null); 429 // Make this inactive for touches in {@link #layoutWord(int,int)}. 430 if (SuggestionStripView.DBG) { 431 mDebugInfoViews.get(positionInStrip).setText(null); 432 } 433 } 434 final int count = Math.min(suggestedWords.size(), countInStrip); 435 for (int indexInSuggestedWords = 0; indexInSuggestedWords < count; 436 indexInSuggestedWords++) { 437 final int positionInStrip = 438 getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords); 439 final TextView wordView = mWordViews.get(positionInStrip); 440 // {@link TextView#getTag()} is used to get the index in suggestedWords at 441 // {@link SuggestionStripView#onClick(View)}. 442 wordView.setTag(indexInSuggestedWords); 443 wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords)); 444 wordView.setTextColor(getSuggestionTextColor(suggestedWords, indexInSuggestedWords)); 445 if (SuggestionStripView.DBG) { 446 mDebugInfoViews.get(positionInStrip).setText( 447 suggestedWords.getDebugString(indexInSuggestedWords)); 448 } 449 } 450 } 451 452 private int layoutPunctuationSuggestionsAndReturnSuggestionCountInStrip( 453 final PunctuationSuggestions punctuationSuggestions, final ViewGroup stripView) { 454 final int countInStrip = Math.min(punctuationSuggestions.size(), PUNCTUATIONS_IN_STRIP); 455 for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { 456 if (positionInStrip != 0) { 457 // Add divider if this isn't the left most suggestion in suggestions strip. 458 addDivider(stripView, mDividerViews.get(positionInStrip)); 459 } 460 461 final TextView wordView = mWordViews.get(positionInStrip); 462 wordView.setEnabled(true); 463 wordView.setTextColor(mColorAutoCorrect); 464 // {@link TextView#getTag()} is used to get the index in suggestedWords at 465 // {@link SuggestionStripView#onClick(View)}. 466 wordView.setTag(positionInStrip); 467 wordView.setText(punctuationSuggestions.getLabel(positionInStrip)); 468 wordView.setTextScaleX(1.0f); 469 wordView.setCompoundDrawables(null, null, null, null); 470 stripView.addView(wordView); 471 setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight); 472 } 473 mMoreSuggestionsAvailable = (punctuationSuggestions.size() > countInStrip); 474 return countInStrip; 475 } 476 477 public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip, 478 final int stripWidth) { 479 final int width = stripWidth - mDividerWidth - mPadding * 2; 480 481 final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save); 482 wordView.setTextColor(mColorTypedWord); 483 final int wordWidth = (int)(width * mCenterSuggestionWeight); 484 final CharSequence wordToSave = getEllipsizedText(word, wordWidth, wordView.getPaint()); 485 final float wordScaleX = wordView.getTextScaleX(); 486 wordView.setText(wordToSave); 487 wordView.setTextScaleX(wordScaleX); 488 setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); 489 490 final TextView hintView = (TextView)addToDictionaryStrip.findViewById( 491 R.id.hint_add_to_dictionary); 492 hintView.setTextColor(mColorAutoCorrect); 493 final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip) 494 == ViewCompat.LAYOUT_DIRECTION_RTL); 495 final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW; 496 final Resources res = addToDictionaryStrip.getResources(); 497 final boolean isRtlSystem = SubtypeLocaleUtils.isRtlLanguage(res.getConfiguration().locale); 498 final CharSequence hintText = res.getText(R.string.hint_add_to_dictionary); 499 final String hintWithArrow = (isRtlLanguage == isRtlSystem) 500 ? (arrow + hintText) : (hintText + arrow); 501 final int hintWidth = width - wordWidth; 502 hintView.setTextScaleX(1.0f); // Reset textScaleX. 503 final float hintScaleX = getTextScaleX(hintWithArrow, hintWidth, hintView.getPaint()); 504 hintView.setText(hintWithArrow); 505 hintView.setTextScaleX(hintScaleX); 506 setLayoutWeight( 507 hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); 508 } 509 510 public void layoutImportantNotice(final View importantNoticeStrip, 511 final String importantNoticeTitle) { 512 final TextView titleView = (TextView)importantNoticeStrip.findViewById( 513 R.id.important_notice_title); 514 final int width = titleView.getWidth() - titleView.getPaddingLeft() 515 - titleView.getPaddingRight(); 516 titleView.setTextColor(mColorAutoCorrect); 517 titleView.setText(importantNoticeTitle); 518 titleView.setTextScaleX(1.0f); // Reset textScaleX. 519 final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint()); 520 titleView.setTextScaleX(titleScaleX); 521 } 522 523 static void setLayoutWeight(final View v, final float weight, final int height) { 524 final ViewGroup.LayoutParams lp = v.getLayoutParams(); 525 if (lp instanceof LinearLayout.LayoutParams) { 526 final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; 527 llp.weight = weight; 528 llp.width = 0; 529 llp.height = height; 530 } 531 } 532 533 private static float getTextScaleX(final CharSequence text, final int maxWidth, 534 final TextPaint paint) { 535 paint.setTextScaleX(1.0f); 536 final int width = getTextWidth(text, paint); 537 if (width <= maxWidth || maxWidth <= 0) { 538 return 1.0f; 539 } 540 return maxWidth / (float)width; 541 } 542 543 private static CharSequence getEllipsizedText(final CharSequence text, final int maxWidth, 544 final TextPaint paint) { 545 if (text == null) { 546 return null; 547 } 548 final float scaleX = getTextScaleX(text, maxWidth, paint); 549 if (scaleX >= MIN_TEXT_XSCALE) { 550 paint.setTextScaleX(scaleX); 551 return text; 552 } 553 554 // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To 555 // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE). 556 final float upscaledWidth = maxWidth / MIN_TEXT_XSCALE; 557 CharSequence ellipsized = TextUtils.ellipsize( 558 text, paint, upscaledWidth, TextUtils.TruncateAt.MIDDLE); 559 // For an unknown reason, ellipsized seems to return a text that does indeed fit inside the 560 // passed width according to paint.measureText, but not according to paint.getTextWidths. 561 // But when rendered, the text seems to actually take up as many pixels as returned by 562 // paint.getTextWidths, hence problem. 563 // To save this case, we compare the measured size of the new text, and if it's too much, 564 // try it again removing the difference. This may still give a text too long by one or 565 // two pixels so we take an additional 2 pixels cushion and call it a day. 566 // TODO: figure out why getTextWidths and measureText don't agree with each other, and 567 // remove the following code. 568 final float ellipsizedTextWidth = getTextWidth(ellipsized, paint); 569 if (upscaledWidth <= ellipsizedTextWidth) { 570 ellipsized = TextUtils.ellipsize( 571 text, paint, upscaledWidth - (ellipsizedTextWidth - upscaledWidth) - 2, 572 TextUtils.TruncateAt.MIDDLE); 573 } 574 paint.setTextScaleX(MIN_TEXT_XSCALE); 575 return ellipsized; 576 } 577 578 private static int getTextWidth(final CharSequence text, final TextPaint paint) { 579 if (TextUtils.isEmpty(text)) { 580 return 0; 581 } 582 final Typeface savedTypeface = paint.getTypeface(); 583 paint.setTypeface(getTextTypeface(text)); 584 final int len = text.length(); 585 final float[] widths = new float[len]; 586 final int count = paint.getTextWidths(text, 0, len, widths); 587 int width = 0; 588 for (int i = 0; i < count; i++) { 589 width += Math.round(widths[i] + 0.5f); 590 } 591 paint.setTypeface(savedTypeface); 592 return width; 593 } 594 595 private static Typeface getTextTypeface(final CharSequence text) { 596 if (!(text instanceof SpannableString)) { 597 return Typeface.DEFAULT; 598 } 599 600 final SpannableString ss = (SpannableString)text; 601 final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class); 602 if (styles.length == 0) { 603 return Typeface.DEFAULT; 604 } 605 606 if (styles[0].getStyle() == Typeface.BOLD) { 607 return Typeface.DEFAULT_BOLD; 608 } 609 // TODO: BOLD_ITALIC, ITALIC case? 610 return Typeface.DEFAULT; 611 } 612} 613