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 SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs, final int defStyle, final ArrayList<TextView> wordViews, final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews)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 getMaxMoreSuggestionsRow()165 public int getMaxMoreSuggestionsRow() { 166 return mMaxMoreSuggestionsRow; 167 } 168 getMoreSuggestionsHeight()169 private int getMoreSuggestionsHeight() { 170 return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap; 171 } 172 setMoreSuggestionsHeight(final int remainingHeight)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 getMoreSuggestionsHint(final Resources res, final float textSize, final int color)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 getStyledSuggestedWord(final SuggestedWords suggestedWords, final int indexInSuggestedWords)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 */ getPositionInSuggestionStrip(final int indexInSuggestedWords, final SuggestedWords suggestedWords)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 shouldOmitTypedWord(final int inputStyle, final boolean gestureFloatingPreviewTextEnabled, final boolean shouldShowUiToAcceptTypedWord)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 getPositionInSuggestionStrip(final int indexInSuggestedWords, final boolean willAutoCorrect, final boolean omitTypedWord, final int centerPositionInStrip, final int typedWordPositionWhenAutoCorrect)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 getSuggestionTextColor(final SuggestedWords suggestedWords, final int indexInSuggestedWords)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 applyAlpha(final int color, final float alpha)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 addDivider(final ViewGroup stripView, final View dividerView)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 */ layoutAndReturnStartIndexOfMoreSuggestions(final SuggestedWords suggestedWords, final ViewGroup stripView, final ViewGroup placerView)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 */ layoutWord(final int positionInStrip, final int width)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 layoutDebugInfo(final int positionInStrip, final ViewGroup placerView, final int x)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 getSuggestionWidth(final int positionInStrip, final int maxWidth)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 getSuggestionWeight(final int positionInStrip)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 setupWordViewsAndReturnStartIndexOfMoreSuggestions( final SuggestedWords suggestedWords, final int maxSuggestionInStrip)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 layoutPunctuationsAndReturnStartIndexOfMoreSuggestions( final PunctuationSuggestions punctuationSuggestions, final ViewGroup stripView)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 layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip)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 layoutImportantNotice(final View importantNoticeStrip, final String importantNoticeTitle)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 setLayoutWeight(final View v, final float weight, final int height)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 getTextScaleX(final CharSequence text, final int maxWidth, final TextPaint paint)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 getEllipsizedText(final CharSequence text, final int maxWidth, final TextPaint paint)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 getTextWidth(final CharSequence text, final TextPaint paint)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 getTextTypeface(final CharSequence text)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