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