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