SuggestionStripLayoutHelper.java revision 2dae79b1966a7970c25c8b79beec1c95c13f6c87
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.accessibility.AccessibilityUtils; 48import com.android.inputmethod.latin.PunctuationSuggestions; 49import com.android.inputmethod.latin.R; 50import com.android.inputmethod.latin.SuggestedWords; 51import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 52import com.android.inputmethod.latin.define.DebugFlags; 53import com.android.inputmethod.latin.utils.AutoCorrectionUtils; 54import com.android.inputmethod.latin.utils.ResourceUtils; 55import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 56import com.android.inputmethod.latin.utils.ViewLayoutUtils; 57 58import java.util.ArrayList; 59 60final class SuggestionStripLayoutHelper { 61 private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3; 62 private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f; 63 private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2; 64 private static final int PUNCTUATIONS_IN_STRIP = 5; 65 private static final float MIN_TEXT_XSCALE = 0.70f; 66 67 public final int mPadding; 68 public final int mDividerWidth; 69 public final int mSuggestionsStripHeight; 70 private final int mSuggestionsCountInStrip; 71 public final int mMoreSuggestionsRowHeight; 72 private int mMaxMoreSuggestionsRow; 73 public final float mMinMoreSuggestionsWidth; 74 public final int mMoreSuggestionsBottomGap; 75 public boolean mMoreSuggestionsAvailable; 76 77 // The index of these {@link ArrayList} is the position in the suggestion strip. The indices 78 // increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. 79 // The position of the most important suggestion is in {@link #mCenterPositionInStrip} 80 private final ArrayList<TextView> mWordViews; 81 private final ArrayList<View> mDividerViews; 82 private final ArrayList<TextView> mDebugInfoViews; 83 84 private final int mColorValidTypedWord; 85 private final int mColorTypedWord; 86 private final int mColorAutoCorrect; 87 private final int mColorSuggested; 88 private final float mAlphaObsoleted; 89 private final float mCenterSuggestionWeight; 90 private final int mCenterPositionInStrip; 91 private final int mTypedWordPositionWhenAutocorrect; 92 private final Drawable mMoreSuggestionsHint; 93 private static final String MORE_SUGGESTIONS_HINT = "\u2026"; 94 private static final String LEFTWARDS_ARROW = "\u2190"; 95 private static final String RIGHTWARDS_ARROW = "\u2192"; 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 int setMoreSuggestionsHeight(final int remainingHeight) { 171 final int currentHeight = getMoreSuggestionsHeight(); 172 if (currentHeight <= remainingHeight) { 173 return currentHeight; 174 } 175 176 mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap) 177 / mMoreSuggestionsRowHeight; 178 final int newHeight = getMoreSuggestionsHeight(); 179 return newHeight; 180 } 181 182 private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize, 183 final int color) { 184 final Paint paint = new Paint(); 185 paint.setAntiAlias(true); 186 paint.setTextAlign(Align.CENTER); 187 paint.setTextSize(textSize); 188 paint.setColor(color); 189 final Rect bounds = new Rect(); 190 paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds); 191 final int width = Math.round(bounds.width() + 0.5f); 192 final int height = Math.round(bounds.height() + 0.5f); 193 final Bitmap buffer = Bitmap.createBitmap(width, (height * 3 / 2), Bitmap.Config.ARGB_8888); 194 final Canvas canvas = new Canvas(buffer); 195 canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint); 196 return new BitmapDrawable(res, buffer); 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 int len = word.length(); 216 final Spannable spannedWord = new SpannableString(word); 217 final int options = mSuggestionStripOptions; 218 if ((isAutoCorrection && (options & AUTO_CORRECT_BOLD) != 0) 219 || (isTypedWordValid && (options & VALID_TYPED_WORD_BOLD) != 0)) { 220 spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 221 } 222 if (isAutoCorrection && (options & AUTO_CORRECT_UNDERLINE) != 0) { 223 spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 224 } 225 return spannedWord; 226 } 227 228 private int getPositionInSuggestionStrip(final int indexInSuggestedWords, 229 final SuggestedWords suggestedWords) { 230 final int indexToDisplayMostImportantSuggestion; 231 final int indexToDisplaySecondMostImportantSuggestion; 232 if (suggestedWords.mWillAutoCorrect) { 233 indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; 234 indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; 235 } else { 236 indexToDisplayMostImportantSuggestion = SuggestedWords.INDEX_OF_TYPED_WORD; 237 indexToDisplaySecondMostImportantSuggestion = SuggestedWords.INDEX_OF_AUTO_CORRECTION; 238 } 239 if (indexInSuggestedWords == indexToDisplayMostImportantSuggestion) { 240 return mCenterPositionInStrip; 241 } 242 if (indexInSuggestedWords == indexToDisplaySecondMostImportantSuggestion) { 243 return mTypedWordPositionWhenAutocorrect; 244 } 245 // If neither of those, the order in the suggestion strip is the same as in SuggestedWords. 246 return indexInSuggestedWords; 247 } 248 249 private int getSuggestionTextColor(final SuggestedWords suggestedWords, 250 final int indexInSuggestedWords) { 251 final int positionInStrip = 252 getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords); 253 // Use identity for strings, not #equals : it's the typed word if it's the same object 254 final boolean isTypedWord = suggestedWords.getInfo(indexInSuggestedWords).isKindOf( 255 SuggestedWordInfo.KIND_TYPED); 256 257 final int color; 258 if (positionInStrip == mCenterPositionInStrip && suggestedWords.mWillAutoCorrect) { 259 color = mColorAutoCorrect; 260 } else if (isTypedWord && suggestedWords.mTypedWordValid) { 261 color = mColorValidTypedWord; 262 } else if (isTypedWord) { 263 color = mColorTypedWord; 264 } else { 265 color = mColorSuggested; 266 } 267 if (DebugFlags.DEBUG_ENABLED && suggestedWords.size() > 1) { 268 // If we auto-correct, then the autocorrection is in slot 0 and the typed word 269 // is in slot 1. 270 if (positionInStrip == mCenterPositionInStrip 271 && AutoCorrectionUtils.shouldBlockAutoCorrectionBySafetyNet( 272 suggestedWords.getLabel(SuggestedWords.INDEX_OF_AUTO_CORRECTION), 273 suggestedWords.getLabel(SuggestedWords.INDEX_OF_TYPED_WORD))) { 274 return 0xFFFF0000; 275 } 276 } 277 278 if (suggestedWords.mIsObsoleteSuggestions && !isTypedWord) { 279 return applyAlpha(color, mAlphaObsoleted); 280 } 281 return color; 282 } 283 284 private static int applyAlpha(final int color, final float alpha) { 285 final int newAlpha = (int)(Color.alpha(color) * alpha); 286 return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); 287 } 288 289 private static void addDivider(final ViewGroup stripView, final View dividerView) { 290 stripView.addView(dividerView); 291 final LinearLayout.LayoutParams params = 292 (LinearLayout.LayoutParams)dividerView.getLayoutParams(); 293 params.gravity = Gravity.CENTER; 294 } 295 296 /** 297 * Layout suggestions to the suggestions strip. And returns the number of suggestions displayed 298 * in the suggestions strip. 299 * 300 * @param suggestedWords suggestions to be shown in the suggestions strip. 301 * @param stripView the suggestions strip view. 302 * @param placerView the view where the debug info will be placed. 303 * @return the number of suggestions displayed in the suggestions strip 304 */ 305 public int layoutAndReturnSuggestionCountInStrip(final SuggestedWords suggestedWords, 306 final ViewGroup stripView, final ViewGroup placerView) { 307 if (suggestedWords.isPunctuationSuggestions()) { 308 return layoutPunctuationSuggestionsAndReturnSuggestionCountInStrip( 309 (PunctuationSuggestions)suggestedWords, stripView); 310 } 311 312 setupWordViewsTextAndColor(suggestedWords, mSuggestionsCountInStrip); 313 final TextView centerWordView = mWordViews.get(mCenterPositionInStrip); 314 final int stripWidth = stripView.getWidth(); 315 final int centerWidth = getSuggestionWidth(mCenterPositionInStrip, stripWidth); 316 final int countInStrip; 317 if (suggestedWords.size() == 1 || getTextScaleX(centerWordView.getText(), centerWidth, 318 centerWordView.getPaint()) < MIN_TEXT_XSCALE) { 319 // Layout only the most relevant suggested word at the center of the suggestion strip 320 // by consolidating all slots in the strip. 321 countInStrip = 1; 322 mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); 323 layoutWord(mCenterPositionInStrip, stripWidth - mPadding); 324 stripView.addView(centerWordView); 325 setLayoutWeight(centerWordView, 1.0f, ViewGroup.LayoutParams.MATCH_PARENT); 326 if (SuggestionStripView.DBG) { 327 layoutDebugInfo(mCenterPositionInStrip, placerView, stripWidth); 328 } 329 } else { 330 countInStrip = mSuggestionsCountInStrip; 331 mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); 332 int x = 0; 333 for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { 334 if (positionInStrip != 0) { 335 final View divider = mDividerViews.get(positionInStrip); 336 // Add divider if this isn't the left most suggestion in suggestions strip. 337 addDivider(stripView, divider); 338 x += divider.getMeasuredWidth(); 339 } 340 341 final int width = getSuggestionWidth(positionInStrip, stripWidth); 342 final TextView wordView = layoutWord(positionInStrip, width); 343 stripView.addView(wordView); 344 setLayoutWeight(wordView, getSuggestionWeight(positionInStrip), 345 ViewGroup.LayoutParams.MATCH_PARENT); 346 x += wordView.getMeasuredWidth(); 347 348 if (SuggestionStripView.DBG) { 349 layoutDebugInfo(positionInStrip, placerView, x); 350 } 351 } 352 } 353 return countInStrip; 354 } 355 356 /** 357 * Format appropriately the suggested word in {@link #mWordViews} specified by 358 * <code>positionInStrip</code>. When the suggested word doesn't exist, the corresponding 359 * {@link TextView} will be disabled and never respond to user interaction. The suggested word 360 * may be shrunk or ellipsized to fit in the specified width. 361 * 362 * The <code>positionInStrip</code> argument is the index in the suggestion strip. The indices 363 * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0. 364 * The position of the most important suggestion is in {@link #mCenterPositionInStrip}. This 365 * usually doesn't match the index in <code>suggedtedWords</code> -- see 366 * {@link #getPositionInSuggestionStrip(int,SuggestedWords)}. 367 * 368 * @param positionInStrip the position in the suggestion strip. 369 * @param width the maximum width for layout in pixels. 370 * @return the {@link TextView} containing the suggested word appropriately formatted. 371 */ 372 private TextView layoutWord(final int positionInStrip, final int width) { 373 final TextView wordView = mWordViews.get(positionInStrip); 374 final CharSequence word = wordView.getText(); 375 if (positionInStrip == mCenterPositionInStrip && mMoreSuggestionsAvailable) { 376 // TODO: This "more suggestions hint" should have a nicely designed icon. 377 wordView.setCompoundDrawablesWithIntrinsicBounds( 378 null, null, null, mMoreSuggestionsHint); 379 // HACK: Align with other TextViews that have no compound drawables. 380 wordView.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight()); 381 } else { 382 wordView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); 383 } 384 // {@link StyleSpan} in a content description may cause an issue of TTS/TalkBack. 385 // Use a simple {@link String} to avoid the issue. 386 wordView.setContentDescription(TextUtils.isEmpty(word) ? null : word.toString()); 387 final CharSequence text = getEllipsizedText(word, width, wordView.getPaint()); 388 final float scaleX = getTextScaleX(word, width, wordView.getPaint()); 389 wordView.setText(text); // TextView.setText() resets text scale x to 1.0. 390 wordView.setTextScaleX(Math.max(scaleX, MIN_TEXT_XSCALE)); 391 // A <code>wordView</code> should be disabled when <code>word</code> is empty in order to 392 // make it unclickable. 393 // With accessibility touch exploration on, <code>wordView</code> should be enabled even 394 // when it is empty to avoid announcing as "disabled". 395 wordView.setEnabled(!TextUtils.isEmpty(word) 396 || AccessibilityUtils.getInstance().isTouchExplorationEnabled()); 397 return wordView; 398 } 399 400 private void layoutDebugInfo(final int positionInStrip, final ViewGroup placerView, 401 final int x) { 402 final TextView debugInfoView = mDebugInfoViews.get(positionInStrip); 403 final CharSequence debugInfo = debugInfoView.getText(); 404 if (debugInfo == null) { 405 return; 406 } 407 placerView.addView(debugInfoView); 408 debugInfoView.measure( 409 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 410 final int infoWidth = debugInfoView.getMeasuredWidth(); 411 final int y = debugInfoView.getMeasuredHeight(); 412 ViewLayoutUtils.placeViewAt( 413 debugInfoView, x - infoWidth, y, infoWidth, debugInfoView.getMeasuredHeight()); 414 } 415 416 private int getSuggestionWidth(final int positionInStrip, final int maxWidth) { 417 final int paddings = mPadding * mSuggestionsCountInStrip; 418 final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1); 419 final int availableWidth = maxWidth - paddings - dividers; 420 return (int)(availableWidth * getSuggestionWeight(positionInStrip)); 421 } 422 423 private float getSuggestionWeight(final int positionInStrip) { 424 if (positionInStrip == mCenterPositionInStrip) { 425 return mCenterSuggestionWeight; 426 } 427 // TODO: Revisit this for cases of 5 or more suggestions 428 return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1); 429 } 430 431 private void setupWordViewsTextAndColor(final SuggestedWords suggestedWords, 432 final int countInStrip) { 433 // Clear all suggestions first 434 for (int positionInStrip = 0; positionInStrip < countInStrip; ++positionInStrip) { 435 final TextView wordView = mWordViews.get(positionInStrip); 436 wordView.setText(null); 437 wordView.setTag(null); 438 // Make this inactive for touches in {@link #layoutWord(int,int)}. 439 if (SuggestionStripView.DBG) { 440 mDebugInfoViews.get(positionInStrip).setText(null); 441 } 442 } 443 final int count = Math.min(suggestedWords.size(), countInStrip); 444 for (int indexInSuggestedWords = 0; indexInSuggestedWords < count; 445 indexInSuggestedWords++) { 446 final int positionInStrip = 447 getPositionInSuggestionStrip(indexInSuggestedWords, suggestedWords); 448 final TextView wordView = mWordViews.get(positionInStrip); 449 // {@link TextView#getTag()} is used to get the index in suggestedWords at 450 // {@link SuggestionStripView#onClick(View)}. 451 wordView.setTag(indexInSuggestedWords); 452 wordView.setText(getStyledSuggestedWord(suggestedWords, indexInSuggestedWords)); 453 wordView.setTextColor(getSuggestionTextColor(suggestedWords, indexInSuggestedWords)); 454 if (SuggestionStripView.DBG) { 455 mDebugInfoViews.get(positionInStrip).setText( 456 suggestedWords.getDebugString(indexInSuggestedWords)); 457 } 458 } 459 } 460 461 private int layoutPunctuationSuggestionsAndReturnSuggestionCountInStrip( 462 final PunctuationSuggestions punctuationSuggestions, final ViewGroup stripView) { 463 final int countInStrip = Math.min(punctuationSuggestions.size(), PUNCTUATIONS_IN_STRIP); 464 for (int positionInStrip = 0; positionInStrip < countInStrip; positionInStrip++) { 465 if (positionInStrip != 0) { 466 // Add divider if this isn't the left most suggestion in suggestions strip. 467 addDivider(stripView, mDividerViews.get(positionInStrip)); 468 } 469 470 final TextView wordView = mWordViews.get(positionInStrip); 471 final String punctuation = punctuationSuggestions.getLabel(positionInStrip); 472 // {@link TextView#getTag()} is used to get the index in suggestedWords at 473 // {@link SuggestionStripView#onClick(View)}. 474 wordView.setTag(positionInStrip); 475 wordView.setText(punctuation); 476 wordView.setContentDescription(punctuation); 477 wordView.setTextScaleX(1.0f); 478 wordView.setCompoundDrawables(null, null, null, null); 479 wordView.setTextColor(mColorAutoCorrect); 480 stripView.addView(wordView); 481 setLayoutWeight(wordView, 1.0f, mSuggestionsStripHeight); 482 } 483 mMoreSuggestionsAvailable = (punctuationSuggestions.size() > countInStrip); 484 return countInStrip; 485 } 486 487 public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip) { 488 final int stripWidth = addToDictionaryStrip.getWidth(); 489 final int width = stripWidth - mDividerWidth - mPadding * 2; 490 491 final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save); 492 wordView.setTextColor(mColorTypedWord); 493 final int wordWidth = (int)(width * mCenterSuggestionWeight); 494 final CharSequence wordToSave = getEllipsizedText(word, wordWidth, wordView.getPaint()); 495 final float wordScaleX = wordView.getTextScaleX(); 496 wordView.setText(wordToSave); 497 wordView.setTextScaleX(wordScaleX); 498 setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); 499 500 final TextView hintView = (TextView)addToDictionaryStrip.findViewById( 501 R.id.hint_add_to_dictionary); 502 hintView.setTextColor(mColorAutoCorrect); 503 final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip) 504 == ViewCompat.LAYOUT_DIRECTION_RTL); 505 final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW; 506 final Resources res = addToDictionaryStrip.getResources(); 507 final boolean isRtlSystem = SubtypeLocaleUtils.isRtlLanguage(res.getConfiguration().locale); 508 final CharSequence hintText = res.getText(R.string.hint_add_to_dictionary); 509 final String hintWithArrow = (isRtlLanguage == isRtlSystem) 510 ? (arrow + hintText) : (hintText + arrow); 511 final int hintWidth = width - wordWidth; 512 hintView.setTextScaleX(1.0f); // Reset textScaleX. 513 final float hintScaleX = getTextScaleX(hintWithArrow, hintWidth, hintView.getPaint()); 514 hintView.setText(hintWithArrow); 515 hintView.setTextScaleX(hintScaleX); 516 setLayoutWeight( 517 hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); 518 } 519 520 public void layoutImportantNotice(final View importantNoticeStrip, 521 final String importantNoticeTitle) { 522 final TextView titleView = (TextView)importantNoticeStrip.findViewById( 523 R.id.important_notice_title); 524 final int width = titleView.getWidth() - titleView.getPaddingLeft() 525 - titleView.getPaddingRight(); 526 titleView.setTextColor(mColorAutoCorrect); 527 titleView.setText(importantNoticeTitle); 528 titleView.setTextScaleX(1.0f); // Reset textScaleX. 529 final float titleScaleX = getTextScaleX(importantNoticeTitle, width, titleView.getPaint()); 530 titleView.setTextScaleX(titleScaleX); 531 } 532 533 static void setLayoutWeight(final View v, final float weight, final int height) { 534 final ViewGroup.LayoutParams lp = v.getLayoutParams(); 535 if (lp instanceof LinearLayout.LayoutParams) { 536 final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; 537 llp.weight = weight; 538 llp.width = 0; 539 llp.height = height; 540 } 541 } 542 543 private static float getTextScaleX(final CharSequence text, final int maxWidth, 544 final TextPaint paint) { 545 paint.setTextScaleX(1.0f); 546 final int width = getTextWidth(text, paint); 547 if (width <= maxWidth || maxWidth <= 0) { 548 return 1.0f; 549 } 550 return maxWidth / (float)width; 551 } 552 553 private static CharSequence getEllipsizedText(final CharSequence text, final int maxWidth, 554 final TextPaint paint) { 555 if (text == null) { 556 return null; 557 } 558 final float scaleX = getTextScaleX(text, maxWidth, paint); 559 if (scaleX >= MIN_TEXT_XSCALE) { 560 paint.setTextScaleX(scaleX); 561 return text; 562 } 563 564 // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To 565 // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE). 566 final float upscaledWidth = maxWidth / MIN_TEXT_XSCALE; 567 CharSequence ellipsized = TextUtils.ellipsize( 568 text, paint, upscaledWidth, TextUtils.TruncateAt.MIDDLE); 569 // For an unknown reason, ellipsized seems to return a text that does indeed fit inside the 570 // passed width according to paint.measureText, but not according to paint.getTextWidths. 571 // But when rendered, the text seems to actually take up as many pixels as returned by 572 // paint.getTextWidths, hence problem. 573 // To save this case, we compare the measured size of the new text, and if it's too much, 574 // try it again removing the difference. This may still give a text too long by one or 575 // two pixels so we take an additional 2 pixels cushion and call it a day. 576 // TODO: figure out why getTextWidths and measureText don't agree with each other, and 577 // remove the following code. 578 final float ellipsizedTextWidth = getTextWidth(ellipsized, paint); 579 if (upscaledWidth <= ellipsizedTextWidth) { 580 ellipsized = TextUtils.ellipsize( 581 text, paint, upscaledWidth - (ellipsizedTextWidth - upscaledWidth) - 2, 582 TextUtils.TruncateAt.MIDDLE); 583 } 584 paint.setTextScaleX(MIN_TEXT_XSCALE); 585 return ellipsized; 586 } 587 588 private static int getTextWidth(final CharSequence text, final TextPaint paint) { 589 if (TextUtils.isEmpty(text)) { 590 return 0; 591 } 592 final Typeface savedTypeface = paint.getTypeface(); 593 paint.setTypeface(getTextTypeface(text)); 594 final int len = text.length(); 595 final float[] widths = new float[len]; 596 final int count = paint.getTextWidths(text, 0, len, widths); 597 int width = 0; 598 for (int i = 0; i < count; i++) { 599 width += Math.round(widths[i] + 0.5f); 600 } 601 paint.setTypeface(savedTypeface); 602 return width; 603 } 604 605 private static Typeface getTextTypeface(final CharSequence text) { 606 if (!(text instanceof SpannableString)) { 607 return Typeface.DEFAULT; 608 } 609 610 final SpannableString ss = (SpannableString)text; 611 final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class); 612 if (styles.length == 0) { 613 return Typeface.DEFAULT; 614 } 615 616 if (styles[0].getStyle() == Typeface.BOLD) { 617 return Typeface.DEFAULT_BOLD; 618 } 619 // TODO: BOLD_ITALIC, ITALIC case? 620 return Typeface.DEFAULT; 621 } 622} 623