1 /* 2 * Copyright (C) 2011 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.GestureDetector; 41 import android.view.Gravity; 42 import android.view.LayoutInflater; 43 import android.view.MotionEvent; 44 import android.view.View; 45 import android.view.View.OnClickListener; 46 import android.view.View.OnLongClickListener; 47 import android.view.ViewGroup; 48 import android.widget.LinearLayout; 49 import android.widget.RelativeLayout; 50 import android.widget.TextView; 51 52 import com.android.inputmethod.keyboard.Keyboard; 53 import com.android.inputmethod.keyboard.KeyboardSwitcher; 54 import com.android.inputmethod.keyboard.MainKeyboardView; 55 import com.android.inputmethod.keyboard.MoreKeysPanel; 56 import com.android.inputmethod.keyboard.ViewLayoutUtils; 57 import com.android.inputmethod.latin.AutoCorrection; 58 import com.android.inputmethod.latin.CollectionUtils; 59 import com.android.inputmethod.latin.Constants; 60 import com.android.inputmethod.latin.LatinImeLogger; 61 import com.android.inputmethod.latin.R; 62 import com.android.inputmethod.latin.ResourceUtils; 63 import com.android.inputmethod.latin.SuggestedWords; 64 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 65 import com.android.inputmethod.latin.Utils; 66 import com.android.inputmethod.latin.define.ProductionFlag; 67 import com.android.inputmethod.latin.suggestions.MoreSuggestions.MoreSuggestionsListener; 68 import com.android.inputmethod.research.ResearchLogger; 69 70 import java.util.ArrayList; 71 72 public final class SuggestionStripView extends RelativeLayout implements OnClickListener, 73 OnLongClickListener { 74 public interface Listener { addWordToUserDictionary(String word)75 public void addWordToUserDictionary(String word); pickSuggestionManually(int index, SuggestedWordInfo word)76 public void pickSuggestionManually(int index, SuggestedWordInfo word); 77 } 78 79 // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}. 80 public static final int MAX_SUGGESTIONS = 18; 81 82 static final boolean DBG = LatinImeLogger.sDBG; 83 84 private final ViewGroup mSuggestionsStrip; 85 MainKeyboardView mMainKeyboardView; 86 87 private final View mMoreSuggestionsContainer; 88 private final MoreSuggestionsView mMoreSuggestionsView; 89 private final MoreSuggestions.Builder mMoreSuggestionsBuilder; 90 91 private final ArrayList<TextView> mWords = CollectionUtils.newArrayList(); 92 private final ArrayList<TextView> mInfos = CollectionUtils.newArrayList(); 93 private final ArrayList<View> mDividers = CollectionUtils.newArrayList(); 94 95 Listener mListener; 96 private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; 97 98 private final SuggestionStripViewParams mParams; 99 private static final float MIN_TEXT_XSCALE = 0.70f; 100 101 private static final class SuggestionStripViewParams { 102 private static final int DEFAULT_SUGGESTIONS_COUNT_IN_STRIP = 3; 103 private static final float DEFAULT_CENTER_SUGGESTION_PERCENTILE = 0.40f; 104 private static final int DEFAULT_MAX_MORE_SUGGESTIONS_ROW = 2; 105 private static final int PUNCTUATIONS_IN_STRIP = 5; 106 107 public final int mPadding; 108 public final int mDividerWidth; 109 public final int mSuggestionsStripHeight; 110 public final int mSuggestionsCountInStrip; 111 public final int mMoreSuggestionsRowHeight; 112 private int mMaxMoreSuggestionsRow; 113 public final float mMinMoreSuggestionsWidth; 114 public final int mMoreSuggestionsBottomGap; 115 116 private final ArrayList<TextView> mWords; 117 private final ArrayList<View> mDividers; 118 private final ArrayList<TextView> mInfos; 119 120 private final int mColorValidTypedWord; 121 private final int mColorTypedWord; 122 private final int mColorAutoCorrect; 123 private final int mColorSuggested; 124 private final float mAlphaObsoleted; 125 private final float mCenterSuggestionWeight; 126 private final int mCenterSuggestionIndex; 127 private final Drawable mMoreSuggestionsHint; 128 private static final String MORE_SUGGESTIONS_HINT = "\u2026"; 129 private static final String LEFTWARDS_ARROW = "\u2190"; 130 131 private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); 132 private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); 133 private static final int AUTO_CORRECT_BOLD = 0x01; 134 private static final int AUTO_CORRECT_UNDERLINE = 0x02; 135 private static final int VALID_TYPED_WORD_BOLD = 0x04; 136 137 private final int mSuggestionStripOption; 138 139 private final ArrayList<CharSequence> mTexts = CollectionUtils.newArrayList(); 140 141 public boolean mMoreSuggestionsAvailable; 142 143 private final TextView mWordToSaveView; 144 private final TextView mLeftwardsArrowView; 145 private final TextView mHintToSaveView; 146 SuggestionStripViewParams(final Context context, final AttributeSet attrs, final int defStyle, final ArrayList<TextView> words, final ArrayList<View> dividers, final ArrayList<TextView> infos)147 public SuggestionStripViewParams(final Context context, final AttributeSet attrs, 148 final int defStyle, final ArrayList<TextView> words, final ArrayList<View> dividers, 149 final ArrayList<TextView> infos) { 150 mWords = words; 151 mDividers = dividers; 152 mInfos = infos; 153 154 final TextView word = words.get(0); 155 final View divider = dividers.get(0); 156 mPadding = word.getCompoundPaddingLeft() + word.getCompoundPaddingRight(); 157 divider.measure( 158 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 159 mDividerWidth = divider.getMeasuredWidth(); 160 161 final Resources res = word.getResources(); 162 mSuggestionsStripHeight = res.getDimensionPixelSize(R.dimen.suggestions_strip_height); 163 164 final TypedArray a = context.obtainStyledAttributes(attrs, 165 R.styleable.SuggestionStripView, defStyle, R.style.SuggestionStripViewStyle); 166 mSuggestionStripOption = a.getInt( 167 R.styleable.SuggestionStripView_suggestionStripOption, 0); 168 final float alphaValidTypedWord = ResourceUtils.getFraction(a, 169 R.styleable.SuggestionStripView_alphaValidTypedWord, 1.0f); 170 final float alphaTypedWord = ResourceUtils.getFraction(a, 171 R.styleable.SuggestionStripView_alphaTypedWord, 1.0f); 172 final float alphaAutoCorrect = ResourceUtils.getFraction(a, 173 R.styleable.SuggestionStripView_alphaAutoCorrect, 1.0f); 174 final float alphaSuggested = ResourceUtils.getFraction(a, 175 R.styleable.SuggestionStripView_alphaSuggested, 1.0f); 176 mAlphaObsoleted = ResourceUtils.getFraction(a, 177 R.styleable.SuggestionStripView_alphaSuggested, 1.0f); 178 mColorValidTypedWord = applyAlpha(a.getColor( 179 R.styleable.SuggestionStripView_colorValidTypedWord, 0), alphaValidTypedWord); 180 mColorTypedWord = applyAlpha(a.getColor( 181 R.styleable.SuggestionStripView_colorTypedWord, 0), alphaTypedWord); 182 mColorAutoCorrect = applyAlpha(a.getColor( 183 R.styleable.SuggestionStripView_colorAutoCorrect, 0), alphaAutoCorrect); 184 mColorSuggested = applyAlpha(a.getColor( 185 R.styleable.SuggestionStripView_colorSuggested, 0), alphaSuggested); 186 mSuggestionsCountInStrip = a.getInt( 187 R.styleable.SuggestionStripView_suggestionsCountInStrip, 188 DEFAULT_SUGGESTIONS_COUNT_IN_STRIP); 189 mCenterSuggestionWeight = ResourceUtils.getFraction(a, 190 R.styleable.SuggestionStripView_centerSuggestionPercentile, 191 DEFAULT_CENTER_SUGGESTION_PERCENTILE); 192 mMaxMoreSuggestionsRow = a.getInt( 193 R.styleable.SuggestionStripView_maxMoreSuggestionsRow, 194 DEFAULT_MAX_MORE_SUGGESTIONS_ROW); 195 mMinMoreSuggestionsWidth = ResourceUtils.getFraction(a, 196 R.styleable.SuggestionStripView_minMoreSuggestionsWidth, 1.0f); 197 a.recycle(); 198 199 mMoreSuggestionsHint = getMoreSuggestionsHint(res, 200 res.getDimension(R.dimen.more_suggestions_hint_text_size), mColorAutoCorrect); 201 mCenterSuggestionIndex = mSuggestionsCountInStrip / 2; 202 mMoreSuggestionsBottomGap = res.getDimensionPixelOffset( 203 R.dimen.more_suggestions_bottom_gap); 204 mMoreSuggestionsRowHeight = res.getDimensionPixelSize( 205 R.dimen.more_suggestions_row_height); 206 207 final LayoutInflater inflater = LayoutInflater.from(context); 208 mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null); 209 mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null); 210 mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null); 211 } 212 getMaxMoreSuggestionsRow()213 public int getMaxMoreSuggestionsRow() { 214 return mMaxMoreSuggestionsRow; 215 } 216 getMoreSuggestionsHeight()217 private int getMoreSuggestionsHeight() { 218 return mMaxMoreSuggestionsRow * mMoreSuggestionsRowHeight + mMoreSuggestionsBottomGap; 219 } 220 setMoreSuggestionsHeight(final int remainingHeight)221 public int setMoreSuggestionsHeight(final int remainingHeight) { 222 final int currentHeight = getMoreSuggestionsHeight(); 223 if (currentHeight <= remainingHeight) { 224 return currentHeight; 225 } 226 227 mMaxMoreSuggestionsRow = (remainingHeight - mMoreSuggestionsBottomGap) 228 / mMoreSuggestionsRowHeight; 229 final int newHeight = getMoreSuggestionsHeight(); 230 return newHeight; 231 } 232 getMoreSuggestionsHint(final Resources res, final float textSize, final int color)233 private static Drawable getMoreSuggestionsHint(final Resources res, final float textSize, 234 final int color) { 235 final Paint paint = new Paint(); 236 paint.setAntiAlias(true); 237 paint.setTextAlign(Align.CENTER); 238 paint.setTextSize(textSize); 239 paint.setColor(color); 240 final Rect bounds = new Rect(); 241 paint.getTextBounds(MORE_SUGGESTIONS_HINT, 0, MORE_SUGGESTIONS_HINT.length(), bounds); 242 final int width = Math.round(bounds.width() + 0.5f); 243 final int height = Math.round(bounds.height() + 0.5f); 244 final Bitmap buffer = Bitmap.createBitmap( 245 width, (height * 3 / 2), Bitmap.Config.ARGB_8888); 246 final Canvas canvas = new Canvas(buffer); 247 canvas.drawText(MORE_SUGGESTIONS_HINT, width / 2, height, paint); 248 return new BitmapDrawable(res, buffer); 249 } 250 getStyledSuggestionWord(final SuggestedWords suggestedWords, final int pos)251 private CharSequence getStyledSuggestionWord(final SuggestedWords suggestedWords, 252 final int pos) { 253 final String word = suggestedWords.getWord(pos); 254 final boolean isAutoCorrect = pos == 1 && suggestedWords.willAutoCorrect(); 255 final boolean isTypedWordValid = pos == 0 && suggestedWords.mTypedWordValid; 256 if (!isAutoCorrect && !isTypedWordValid) 257 return word; 258 259 final int len = word.length(); 260 final Spannable spannedWord = new SpannableString(word); 261 final int option = mSuggestionStripOption; 262 if ((isAutoCorrect && (option & AUTO_CORRECT_BOLD) != 0) 263 || (isTypedWordValid && (option & VALID_TYPED_WORD_BOLD) != 0)) { 264 spannedWord.setSpan(BOLD_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 265 } 266 if (isAutoCorrect && (option & AUTO_CORRECT_UNDERLINE) != 0) { 267 spannedWord.setSpan(UNDERLINE_SPAN, 0, len, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 268 } 269 return spannedWord; 270 } 271 getWordPosition(final int index, final SuggestedWords suggestedWords)272 private int getWordPosition(final int index, final SuggestedWords suggestedWords) { 273 // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more 274 // suggestions. 275 final int centerPos = suggestedWords.willAutoCorrect() ? 1 : 0; 276 if (index == mCenterSuggestionIndex) { 277 return centerPos; 278 } else if (index == centerPos) { 279 return mCenterSuggestionIndex; 280 } else { 281 return index; 282 } 283 } 284 getSuggestionTextColor(final int index, final SuggestedWords suggestedWords, final int pos)285 private int getSuggestionTextColor(final int index, final SuggestedWords suggestedWords, 286 final int pos) { 287 // TODO: Need to revisit this logic with bigram suggestions 288 final boolean isSuggested = (pos != 0); 289 290 final int color; 291 if (index == mCenterSuggestionIndex && suggestedWords.willAutoCorrect()) { 292 color = mColorAutoCorrect; 293 } else if (index == mCenterSuggestionIndex && suggestedWords.mTypedWordValid) { 294 color = mColorValidTypedWord; 295 } else if (isSuggested) { 296 color = mColorSuggested; 297 } else { 298 color = mColorTypedWord; 299 } 300 if (LatinImeLogger.sDBG && suggestedWords.size() > 1) { 301 // If we auto-correct, then the autocorrection is in slot 0 and the typed word 302 // is in slot 1. 303 if (index == mCenterSuggestionIndex 304 && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet( 305 suggestedWords.getWord(1), suggestedWords.getWord(0))) { 306 return 0xFFFF0000; 307 } 308 } 309 310 if (suggestedWords.mIsObsoleteSuggestions && isSuggested) { 311 return applyAlpha(color, mAlphaObsoleted); 312 } else { 313 return color; 314 } 315 } 316 applyAlpha(final int color, final float alpha)317 private static int applyAlpha(final int color, final float alpha) { 318 final int newAlpha = (int)(Color.alpha(color) * alpha); 319 return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); 320 } 321 addDivider(final ViewGroup stripView, final View divider)322 private static void addDivider(final ViewGroup stripView, final View divider) { 323 stripView.addView(divider); 324 final LinearLayout.LayoutParams params = 325 (LinearLayout.LayoutParams)divider.getLayoutParams(); 326 params.gravity = Gravity.CENTER; 327 } 328 layout(final SuggestedWords suggestedWords, final ViewGroup stripView, final ViewGroup placer, final int stripWidth)329 public void layout(final SuggestedWords suggestedWords, final ViewGroup stripView, 330 final ViewGroup placer, final int stripWidth) { 331 if (suggestedWords.mIsPunctuationSuggestions) { 332 layoutPunctuationSuggestions(suggestedWords, stripView); 333 return; 334 } 335 336 final int countInStrip = mSuggestionsCountInStrip; 337 setupTexts(suggestedWords, countInStrip); 338 mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip); 339 int x = 0; 340 for (int index = 0; index < countInStrip; index++) { 341 final int pos = getWordPosition(index, suggestedWords); 342 343 if (index != 0) { 344 final View divider = mDividers.get(pos); 345 // Add divider if this isn't the left most suggestion in suggestions strip. 346 addDivider(stripView, divider); 347 x += divider.getMeasuredWidth(); 348 } 349 350 final CharSequence styled = mTexts.get(pos); 351 final TextView word = mWords.get(pos); 352 if (index == mCenterSuggestionIndex && mMoreSuggestionsAvailable) { 353 // TODO: This "more suggestions hint" should have nicely designed icon. 354 word.setCompoundDrawablesWithIntrinsicBounds( 355 null, null, null, mMoreSuggestionsHint); 356 // HACK: To align with other TextView that has no compound drawables. 357 word.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight()); 358 } else { 359 word.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); 360 } 361 362 // Disable this suggestion if the suggestion is null or empty. 363 word.setEnabled(!TextUtils.isEmpty(styled)); 364 word.setTextColor(getSuggestionTextColor(index, suggestedWords, pos)); 365 final int width = getSuggestionWidth(index, stripWidth); 366 final CharSequence text = getEllipsizedText(styled, width, word.getPaint()); 367 final float scaleX = word.getTextScaleX(); 368 word.setText(text); // TextView.setText() resets text scale x to 1.0. 369 word.setTextScaleX(scaleX); 370 stripView.addView(word); 371 setLayoutWeight( 372 word, getSuggestionWeight(index), ViewGroup.LayoutParams.MATCH_PARENT); 373 x += word.getMeasuredWidth(); 374 375 if (DBG && pos < suggestedWords.size()) { 376 final String debugInfo = Utils.getDebugInfo(suggestedWords, pos); 377 if (debugInfo != null) { 378 final TextView info = mInfos.get(pos); 379 info.setText(debugInfo); 380 placer.addView(info); 381 info.measure(ViewGroup.LayoutParams.WRAP_CONTENT, 382 ViewGroup.LayoutParams.WRAP_CONTENT); 383 final int infoWidth = info.getMeasuredWidth(); 384 final int y = info.getMeasuredHeight(); 385 ViewLayoutUtils.placeViewAt( 386 info, x - infoWidth, y, infoWidth, info.getMeasuredHeight()); 387 } 388 } 389 } 390 } 391 getSuggestionWidth(final int index, final int maxWidth)392 private int getSuggestionWidth(final int index, final int maxWidth) { 393 final int paddings = mPadding * mSuggestionsCountInStrip; 394 final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1); 395 final int availableWidth = maxWidth - paddings - dividers; 396 return (int)(availableWidth * getSuggestionWeight(index)); 397 } 398 getSuggestionWeight(final int index)399 private float getSuggestionWeight(final int index) { 400 if (index == mCenterSuggestionIndex) { 401 return mCenterSuggestionWeight; 402 } else { 403 // TODO: Revisit this for cases of 5 or more suggestions 404 return (1.0f - mCenterSuggestionWeight) / (mSuggestionsCountInStrip - 1); 405 } 406 } 407 setupTexts(final SuggestedWords suggestedWords, final int countInStrip)408 private void setupTexts(final SuggestedWords suggestedWords, final int countInStrip) { 409 mTexts.clear(); 410 final int count = Math.min(suggestedWords.size(), countInStrip); 411 for (int pos = 0; pos < count; pos++) { 412 final CharSequence styled = getStyledSuggestionWord(suggestedWords, pos); 413 mTexts.add(styled); 414 } 415 for (int pos = count; pos < countInStrip; pos++) { 416 // Make this inactive for touches in layout(). 417 mTexts.add(null); 418 } 419 } 420 layoutPunctuationSuggestions(final SuggestedWords suggestedWords, final ViewGroup stripView)421 private void layoutPunctuationSuggestions(final SuggestedWords suggestedWords, 422 final ViewGroup stripView) { 423 final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP); 424 for (int index = 0; index < countInStrip; index++) { 425 if (index != 0) { 426 // Add divider if this isn't the left most suggestion in suggestions strip. 427 addDivider(stripView, mDividers.get(index)); 428 } 429 430 final TextView word = mWords.get(index); 431 word.setEnabled(true); 432 word.setTextColor(mColorAutoCorrect); 433 final String text = suggestedWords.getWord(index); 434 word.setText(text); 435 word.setTextScaleX(1.0f); 436 word.setCompoundDrawables(null, null, null, null); 437 stripView.addView(word); 438 setLayoutWeight(word, 1.0f, mSuggestionsStripHeight); 439 } 440 mMoreSuggestionsAvailable = false; 441 } 442 layoutAddToDictionaryHint(final String word, final ViewGroup stripView, final int stripWidth, final CharSequence hintText, final OnClickListener listener)443 public void layoutAddToDictionaryHint(final String word, final ViewGroup stripView, 444 final int stripWidth, final CharSequence hintText, final OnClickListener listener) { 445 final int width = stripWidth - mDividerWidth - mPadding * 2; 446 447 final TextView wordView = mWordToSaveView; 448 wordView.setTextColor(mColorTypedWord); 449 final int wordWidth = (int)(width * mCenterSuggestionWeight); 450 final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint()); 451 final float wordScaleX = wordView.getTextScaleX(); 452 wordView.setTag(word); 453 wordView.setText(text); 454 wordView.setTextScaleX(wordScaleX); 455 stripView.addView(wordView); 456 setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); 457 458 stripView.addView(mDividers.get(0)); 459 460 final TextView leftArrowView = mLeftwardsArrowView; 461 leftArrowView.setTextColor(mColorAutoCorrect); 462 leftArrowView.setText(LEFTWARDS_ARROW); 463 stripView.addView(leftArrowView); 464 465 final TextView hintView = mHintToSaveView; 466 hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL); 467 hintView.setTextColor(mColorAutoCorrect); 468 final int hintWidth = width - wordWidth - leftArrowView.getWidth(); 469 final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint()); 470 hintView.setText(hintText); 471 hintView.setTextScaleX(hintScaleX); 472 stripView.addView(hintView); 473 setLayoutWeight( 474 hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT); 475 476 wordView.setOnClickListener(listener); 477 leftArrowView.setOnClickListener(listener); 478 hintView.setOnClickListener(listener); 479 } 480 getAddToDictionaryWord()481 public CharSequence getAddToDictionaryWord() { 482 return (CharSequence)mWordToSaveView.getTag(); 483 } 484 isAddToDictionaryShowing(final View v)485 public boolean isAddToDictionaryShowing(final View v) { 486 return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView; 487 } 488 setLayoutWeight(final View v, final float weight, final int height)489 private static void setLayoutWeight(final View v, final float weight, final int height) { 490 final ViewGroup.LayoutParams lp = v.getLayoutParams(); 491 if (lp instanceof LinearLayout.LayoutParams) { 492 final LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; 493 llp.weight = weight; 494 llp.width = 0; 495 llp.height = height; 496 } 497 } 498 getTextScaleX(final CharSequence text, final int maxWidth, final TextPaint paint)499 private static float getTextScaleX(final CharSequence text, final int maxWidth, 500 final TextPaint paint) { 501 paint.setTextScaleX(1.0f); 502 final int width = getTextWidth(text, paint); 503 if (width <= maxWidth) { 504 return 1.0f; 505 } 506 return maxWidth / (float)width; 507 } 508 getEllipsizedText(final CharSequence text, final int maxWidth, final TextPaint paint)509 private static CharSequence getEllipsizedText(final CharSequence text, final int maxWidth, 510 final TextPaint paint) { 511 if (text == null) return null; 512 paint.setTextScaleX(1.0f); 513 final int width = getTextWidth(text, paint); 514 if (width <= maxWidth) { 515 return text; 516 } 517 final float scaleX = maxWidth / (float)width; 518 if (scaleX >= MIN_TEXT_XSCALE) { 519 paint.setTextScaleX(scaleX); 520 return text; 521 } 522 523 // Note that TextUtils.ellipsize() use text-x-scale as 1.0 if ellipsize is needed. To 524 // get squeezed and ellipsized text, passes enlarged width (maxWidth / MIN_TEXT_XSCALE). 525 final CharSequence ellipsized = TextUtils.ellipsize( 526 text, paint, maxWidth / MIN_TEXT_XSCALE, TextUtils.TruncateAt.MIDDLE); 527 paint.setTextScaleX(MIN_TEXT_XSCALE); 528 return ellipsized; 529 } 530 getTextWidth(final CharSequence text, final TextPaint paint)531 private static int getTextWidth(final CharSequence text, final TextPaint paint) { 532 if (TextUtils.isEmpty(text)) return 0; 533 final Typeface savedTypeface = paint.getTypeface(); 534 paint.setTypeface(getTextTypeface(text)); 535 final int len = text.length(); 536 final float[] widths = new float[len]; 537 final int count = paint.getTextWidths(text, 0, len, widths); 538 int width = 0; 539 for (int i = 0; i < count; i++) { 540 width += Math.round(widths[i] + 0.5f); 541 } 542 paint.setTypeface(savedTypeface); 543 return width; 544 } 545 getTextTypeface(final CharSequence text)546 private static Typeface getTextTypeface(final CharSequence text) { 547 if (!(text instanceof SpannableString)) 548 return Typeface.DEFAULT; 549 550 final SpannableString ss = (SpannableString)text; 551 final StyleSpan[] styles = ss.getSpans(0, text.length(), StyleSpan.class); 552 if (styles.length == 0) 553 return Typeface.DEFAULT; 554 555 switch (styles[0].getStyle()) { 556 case Typeface.BOLD: return Typeface.DEFAULT_BOLD; 557 // TODO: BOLD_ITALIC, ITALIC case? 558 default: return Typeface.DEFAULT; 559 } 560 } 561 } 562 563 /** 564 * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user. 565 * @param context 566 * @param attrs 567 */ SuggestionStripView(final Context context, final AttributeSet attrs)568 public SuggestionStripView(final Context context, final AttributeSet attrs) { 569 this(context, attrs, R.attr.suggestionStripViewStyle); 570 } 571 SuggestionStripView(final Context context, final AttributeSet attrs, final int defStyle)572 public SuggestionStripView(final Context context, final AttributeSet attrs, 573 final int defStyle) { 574 super(context, attrs, defStyle); 575 576 final LayoutInflater inflater = LayoutInflater.from(context); 577 inflater.inflate(R.layout.suggestions_strip, this); 578 579 mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip); 580 for (int pos = 0; pos < MAX_SUGGESTIONS; pos++) { 581 final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null); 582 word.setTag(pos); 583 word.setOnClickListener(this); 584 word.setOnLongClickListener(this); 585 mWords.add(word); 586 final View divider = inflater.inflate(R.layout.suggestion_divider, null); 587 divider.setTag(pos); 588 divider.setOnClickListener(this); 589 mDividers.add(divider); 590 mInfos.add((TextView)inflater.inflate(R.layout.suggestion_info, null)); 591 } 592 593 mParams = new SuggestionStripViewParams( 594 context, attrs, defStyle, mWords, mDividers, mInfos); 595 596 mMoreSuggestionsContainer = inflater.inflate(R.layout.more_suggestions, null); 597 mMoreSuggestionsView = (MoreSuggestionsView)mMoreSuggestionsContainer 598 .findViewById(R.id.more_suggestions_view); 599 mMoreSuggestionsBuilder = new MoreSuggestions.Builder(context, mMoreSuggestionsView); 600 601 final Resources res = context.getResources(); 602 mMoreSuggestionsModalTolerance = res.getDimensionPixelOffset( 603 R.dimen.more_suggestions_modal_tolerance); 604 mMoreSuggestionsSlidingDetector = new GestureDetector( 605 context, mMoreSuggestionsSlidingListener); 606 } 607 608 /** 609 * A connection back to the input method. 610 * @param listener 611 */ setListener(final Listener listener, final View inputView)612 public void setListener(final Listener listener, final View inputView) { 613 mListener = listener; 614 mMainKeyboardView = (MainKeyboardView)inputView.findViewById(R.id.keyboard_view); 615 } 616 setSuggestions(final SuggestedWords suggestedWords)617 public void setSuggestions(final SuggestedWords suggestedWords) { 618 clear(); 619 mSuggestedWords = suggestedWords; 620 mParams.layout(mSuggestedWords, mSuggestionsStrip, this, getWidth()); 621 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 622 ResearchLogger.suggestionStripView_setSuggestions(mSuggestedWords); 623 } 624 } 625 setMoreSuggestionsHeight(final int remainingHeight)626 public int setMoreSuggestionsHeight(final int remainingHeight) { 627 return mParams.setMoreSuggestionsHeight(remainingHeight); 628 } 629 isShowingAddToDictionaryHint()630 public boolean isShowingAddToDictionaryHint() { 631 return mSuggestionsStrip.getChildCount() > 0 632 && mParams.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0)); 633 } 634 showAddToDictionaryHint(final String word, final CharSequence hintText)635 public void showAddToDictionaryHint(final String word, final CharSequence hintText) { 636 clear(); 637 mParams.layoutAddToDictionaryHint(word, mSuggestionsStrip, getWidth(), hintText, this); 638 } 639 dismissAddToDictionaryHint()640 public boolean dismissAddToDictionaryHint() { 641 if (isShowingAddToDictionaryHint()) { 642 clear(); 643 return true; 644 } 645 return false; 646 } 647 clear()648 public void clear() { 649 mSuggestionsStrip.removeAllViews(); 650 removeAllViews(); 651 addView(mSuggestionsStrip); 652 dismissMoreSuggestions(); 653 } 654 655 private final MoreSuggestionsListener mMoreSuggestionsListener = new MoreSuggestionsListener() { 656 @Override 657 public void onSuggestionSelected(final int index, final SuggestedWordInfo wordInfo) { 658 mListener.pickSuggestionManually(index, wordInfo); 659 dismissMoreSuggestions(); 660 } 661 662 @Override 663 public void onCancelInput() { 664 dismissMoreSuggestions(); 665 } 666 }; 667 668 private final MoreKeysPanel.Controller mMoreSuggestionsController = 669 new MoreKeysPanel.Controller() { 670 @Override 671 public boolean onDismissMoreKeysPanel() { 672 return mMainKeyboardView.onDismissMoreKeysPanel(); 673 } 674 675 @Override 676 public void onShowMoreKeysPanel(final MoreKeysPanel panel) { 677 mMainKeyboardView.onShowMoreKeysPanel(panel); 678 } 679 680 @Override 681 public void onCancelMoreKeysPanel() { 682 dismissMoreSuggestions(); 683 } 684 }; 685 dismissMoreSuggestions()686 boolean dismissMoreSuggestions() { 687 return mMoreSuggestionsView.dismissMoreKeysPanel(); 688 } 689 690 @Override onLongClick(final View view)691 public boolean onLongClick(final View view) { 692 KeyboardSwitcher.getInstance().hapticAndAudioFeedback(Constants.NOT_A_CODE); 693 return showMoreSuggestions(); 694 } 695 showMoreSuggestions()696 boolean showMoreSuggestions() { 697 final Keyboard parentKeyboard = KeyboardSwitcher.getInstance().getKeyboard(); 698 if (parentKeyboard == null) { 699 return false; 700 } 701 final SuggestionStripViewParams params = mParams; 702 if (!params.mMoreSuggestionsAvailable) { 703 return false; 704 } 705 final int stripWidth = getWidth(); 706 final View container = mMoreSuggestionsContainer; 707 final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight(); 708 final MoreSuggestions.Builder builder = mMoreSuggestionsBuilder; 709 builder.layout(mSuggestedWords, params.mSuggestionsCountInStrip, maxWidth, 710 (int)(maxWidth * params.mMinMoreSuggestionsWidth), 711 params.getMaxMoreSuggestionsRow(), parentKeyboard); 712 mMoreSuggestionsView.setKeyboard(builder.build()); 713 container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 714 715 final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView; 716 final int pointX = stripWidth / 2; 717 final int pointY = -params.mMoreSuggestionsBottomGap; 718 moreKeysPanel.showMoreKeysPanel(this, mMoreSuggestionsController, pointX, pointY, 719 mMoreSuggestionsListener); 720 mMoreSuggestionsMode = MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING; 721 mOriginX = mLastX; 722 mOriginY = mLastY; 723 for (int i = 0; i < params.mSuggestionsCountInStrip; i++) { 724 mWords.get(i).setPressed(false); 725 } 726 return true; 727 } 728 729 // Working variables for onLongClick and dispatchTouchEvent. 730 private int mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE; 731 private static final int MORE_SUGGESTIONS_IN_MODAL_MODE = 0; 732 private static final int MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING = 1; 733 private static final int MORE_SUGGESTIONS_IN_SLIDING_MODE = 2; 734 private int mLastX; 735 private int mLastY; 736 private int mOriginX; 737 private int mOriginY; 738 private final int mMoreSuggestionsModalTolerance; 739 private final GestureDetector mMoreSuggestionsSlidingDetector; 740 private final GestureDetector.OnGestureListener mMoreSuggestionsSlidingListener = 741 new GestureDetector.SimpleOnGestureListener() { 742 @Override 743 public boolean onScroll(MotionEvent down, MotionEvent me, float deltaX, float deltaY) { 744 final float dy = me.getY() - down.getY(); 745 if (deltaY > 0 && dy < 0) { 746 return showMoreSuggestions(); 747 } 748 return false; 749 } 750 }; 751 752 @Override dispatchTouchEvent(final MotionEvent me)753 public boolean dispatchTouchEvent(final MotionEvent me) { 754 if (!mMoreSuggestionsView.isShowingInParent()) { 755 mLastX = (int)me.getX(); 756 mLastY = (int)me.getY(); 757 if (mMoreSuggestionsSlidingDetector.onTouchEvent(me)) { 758 return true; 759 } 760 return super.dispatchTouchEvent(me); 761 } 762 763 final MoreKeysPanel moreKeysPanel = mMoreSuggestionsView; 764 final int action = me.getAction(); 765 final long eventTime = me.getEventTime(); 766 final int index = me.getActionIndex(); 767 final int id = me.getPointerId(index); 768 final int x = (int)me.getX(index); 769 final int y = (int)me.getY(index); 770 final int translatedX = moreKeysPanel.translateX(x); 771 final int translatedY = moreKeysPanel.translateY(y); 772 773 if (mMoreSuggestionsMode == MORE_SUGGESTIONS_CHECKING_MODAL_OR_SLIDING) { 774 if (Math.abs(x - mOriginX) >= mMoreSuggestionsModalTolerance 775 || mOriginY - y >= mMoreSuggestionsModalTolerance) { 776 // Decided to be in the sliding input mode only when the touch point has been moved 777 // upward. 778 mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_SLIDING_MODE; 779 } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) { 780 // Decided to be in the modal input mode 781 mMoreSuggestionsMode = MORE_SUGGESTIONS_IN_MODAL_MODE; 782 mMoreSuggestionsView.adjustVerticalCorrectionForModalMode(); 783 } 784 return true; 785 } 786 787 // MORE_SUGGESTIONS_IN_SLIDING_MODE 788 mMoreSuggestionsView.processMotionEvent(action, translatedX, translatedY, id, eventTime); 789 return true; 790 } 791 792 @Override onClick(final View view)793 public void onClick(final View view) { 794 if (mParams.isAddToDictionaryShowing(view)) { 795 mListener.addWordToUserDictionary(mParams.getAddToDictionaryWord().toString()); 796 clear(); 797 return; 798 } 799 800 final Object tag = view.getTag(); 801 if (!(tag instanceof Integer)) 802 return; 803 final int index = (Integer) tag; 804 if (index >= mSuggestedWords.size()) 805 return; 806 807 final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index); 808 mListener.pickSuggestionManually(index, wordInfo); 809 } 810 811 @Override onDetachedFromWindow()812 protected void onDetachedFromWindow() { 813 super.onDetachedFromWindow(); 814 dismissMoreSuggestions(); 815 } 816 } 817