SuggestionStripView.java revision 86e815a142c8aa13213151e381a8a24ef23073d3
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.graphics.Color; 22import android.graphics.Typeface; 23import android.os.Handler; 24import android.os.Message; 25import android.text.Spannable; 26import android.text.SpannableString; 27import android.text.Spanned; 28import android.text.TextUtils; 29import android.text.style.BackgroundColorSpan; 30import android.text.style.CharacterStyle; 31import android.text.style.ForegroundColorSpan; 32import android.text.style.StyleSpan; 33import android.text.style.UnderlineSpan; 34import android.util.AttributeSet; 35import android.view.Gravity; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.View.OnClickListener; 39import android.view.View.OnLongClickListener; 40import android.view.ViewGroup; 41import android.widget.LinearLayout; 42import android.widget.PopupWindow; 43import android.widget.TextView; 44 45import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 46 47import java.util.ArrayList; 48import java.util.List; 49 50public class CandidateView extends LinearLayout implements OnClickListener, OnLongClickListener { 51 52 public interface Listener { 53 public boolean addWordToDictionary(String word); 54 public void pickSuggestionManually(int index, CharSequence word); 55 } 56 57 private static final CharacterStyle BOLD_SPAN = new StyleSpan(Typeface.BOLD); 58 private static final CharacterStyle UNDERLINE_SPAN = new UnderlineSpan(); 59 // The maximum number of suggestions available. See {@link Suggest#mPrefMaxSuggestions}. 60 private static final int MAX_SUGGESTIONS = 18; 61 private static final int UNSPECIFIED_MEASURESPEC = MeasureSpec.makeMeasureSpec( 62 0, MeasureSpec.UNSPECIFIED); 63 64 private static final boolean DBG = LatinImeLogger.sDBG; 65 66 private static final int NUM_CANDIDATES_IN_STRIP = 3; 67 private final View mExpandCandidatesPane; 68 private final View mCloseCandidatesPane; 69 private ViewGroup mCandidatesPane; 70 private ViewGroup mCandidatesPaneContainer; 71 private View mKeyboardView; 72 private final ArrayList<TextView> mWords = new ArrayList<TextView>(); 73 private final ArrayList<View> mDividers = new ArrayList<View>(); 74 private final int mCandidatePadding; 75 private final int mCandidateStripHeight; 76 private final boolean mConfigCandidateHighlightFontColorEnabled; 77 private final CharacterStyle mInvertedForegroundColorSpan; 78 private final CharacterStyle mInvertedBackgroundColorSpan; 79 private final int mColorTypedWord; 80 private final int mColorAutoCorrect; 81 private final int mColorSuggestedCandidate; 82 private final PopupWindow mPreviewPopup; 83 private final TextView mPreviewText; 84 85 private Listener mListener; 86 private SuggestedWords mSuggestions = SuggestedWords.EMPTY; 87 private boolean mShowingAutoCorrectionInverted; 88 private boolean mShowingAddToDictionary; 89 90 private final UiHandler mHandler = new UiHandler(); 91 92 private class UiHandler extends Handler { 93 private static final int MSG_HIDE_PREVIEW = 0; 94 private static final int MSG_UPDATE_SUGGESTION = 1; 95 96 private static final long DELAY_HIDE_PREVIEW = 1000; 97 private static final long DELAY_UPDATE_SUGGESTION = 300; 98 99 @Override 100 public void dispatchMessage(Message msg) { 101 switch (msg.what) { 102 case MSG_HIDE_PREVIEW: 103 hidePreview(); 104 break; 105 case MSG_UPDATE_SUGGESTION: 106 updateSuggestions(); 107 break; 108 } 109 } 110 111 public void postHidePreview() { 112 cancelHidePreview(); 113 sendMessageDelayed(obtainMessage(MSG_HIDE_PREVIEW), DELAY_HIDE_PREVIEW); 114 } 115 116 public void cancelHidePreview() { 117 removeMessages(MSG_HIDE_PREVIEW); 118 } 119 120 public void postUpdateSuggestions() { 121 cancelUpdateSuggestions(); 122 sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION), 123 DELAY_UPDATE_SUGGESTION); 124 } 125 126 public void cancelUpdateSuggestions() { 127 removeMessages(MSG_UPDATE_SUGGESTION); 128 } 129 130 public void cancelAllMessages() { 131 cancelHidePreview(); 132 cancelUpdateSuggestions(); 133 } 134 } 135 136 /** 137 * Construct a CandidateView for showing suggested words for completion. 138 * @param context 139 * @param attrs 140 */ 141 public CandidateView(Context context, AttributeSet attrs) { 142 super(context, attrs); 143 144 Resources res = context.getResources(); 145 LayoutInflater inflater = LayoutInflater.from(context); 146 inflater.inflate(R.layout.candidates_strip, this); 147 148 mPreviewPopup = new PopupWindow(context); 149 mPreviewText = (TextView) inflater.inflate(R.layout.candidate_preview, null); 150 mPreviewPopup.setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, 151 ViewGroup.LayoutParams.WRAP_CONTENT); 152 mPreviewPopup.setContentView(mPreviewText); 153 mPreviewPopup.setBackgroundDrawable(null); 154 mConfigCandidateHighlightFontColorEnabled = 155 res.getBoolean(R.bool.config_candidate_highlight_font_color_enabled); 156 mColorTypedWord = res.getColor(R.color.candidate_typed_word); 157 mColorAutoCorrect = res.getColor(R.color.candidate_auto_correct); 158 mColorSuggestedCandidate = res.getColor(R.color.candidate_suggested); 159 mInvertedForegroundColorSpan = new ForegroundColorSpan(mColorTypedWord ^ 0x00ffffff); 160 mInvertedBackgroundColorSpan = new BackgroundColorSpan(mColorTypedWord); 161 162 mCandidatePadding = res.getDimensionPixelOffset(R.dimen.candidate_padding); 163 mCandidateStripHeight = res.getDimensionPixelOffset(R.dimen.candidate_strip_height); 164 for (int i = 0; i < MAX_SUGGESTIONS; i++) { 165 final TextView tv; 166 switch (i) { 167 case 0: 168 tv = (TextView)findViewById(R.id.candidate_left); 169 tv.setPadding(mCandidatePadding, 0, 0, 0); 170 break; 171 case 1: 172 tv = (TextView)findViewById(R.id.candidate_center); 173 break; 174 case 2: 175 tv = (TextView)findViewById(R.id.candidate_right); 176 break; 177 default: 178 tv = (TextView)inflater.inflate(R.layout.candidate, null); 179 break; 180 } 181 if (i < NUM_CANDIDATES_IN_STRIP) 182 setLayoutWeight(tv, 1.0f); 183 tv.setTag(i); 184 tv.setOnClickListener(this); 185 if (i == 0) 186 tv.setOnLongClickListener(this); 187 mWords.add(tv); 188 if (i > 0) { 189 final View divider = inflater.inflate(R.layout.candidate_divider, null); 190 divider.measure(UNSPECIFIED_MEASURESPEC, UNSPECIFIED_MEASURESPEC); 191 mDividers.add(divider); 192 } 193 } 194 195 mExpandCandidatesPane = findViewById(R.id.expand_candidates_pane); 196 mExpandCandidatesPane.setOnClickListener(new OnClickListener() { 197 @Override 198 public void onClick(View view) { 199 expandCandidatesPane(); 200 } 201 }); 202 mCloseCandidatesPane = findViewById(R.id.close_candidates_pane); 203 mCloseCandidatesPane.setOnClickListener(new OnClickListener() { 204 @Override 205 public void onClick(View view) { 206 closeCandidatesPane(); 207 } 208 }); 209 } 210 211 /** 212 * A connection back to the input method. 213 * @param listener 214 */ 215 public void setListener(Listener listener, View inputView) { 216 mListener = listener; 217 mKeyboardView = inputView.findViewById(R.id.keyboard_view); 218 mCandidatesPane = (ViewGroup)inputView.findViewById(R.id.candidates_pane); 219 mCandidatesPane.setOnClickListener(this); 220 mCandidatesPaneContainer = (ViewGroup)inputView.findViewById( 221 R.id.candidates_pane_container); 222 } 223 224 public void setSuggestions(SuggestedWords suggestions) { 225 if (suggestions == null) 226 return; 227 mSuggestions = suggestions; 228 if (mShowingAutoCorrectionInverted) { 229 mHandler.postUpdateSuggestions(); 230 } else { 231 updateSuggestions(); 232 } 233 } 234 235 private static void setLayoutWeight(View v, float weight) { 236 ViewGroup.LayoutParams lp = v.getLayoutParams(); 237 if (lp instanceof LinearLayout.LayoutParams) { 238 LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams)lp; 239 llp.width = 0; 240 llp.weight = weight; 241 } 242 } 243 244 private CharSequence getStyledCandidateWord(CharSequence word, boolean isAutoCorrect) { 245 if (!isAutoCorrect) 246 return word; 247 final CharacterStyle style = mConfigCandidateHighlightFontColorEnabled ? BOLD_SPAN 248 : UNDERLINE_SPAN; 249 final Spannable spannedWord = new SpannableString(word); 250 spannedWord.setSpan(style, 0, word.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 251 return spannedWord; 252 } 253 254 private int getCandidateTextColor(boolean isAutoCorrect, boolean isSuggestedCandidate, 255 SuggestedWordInfo info) { 256 final int color; 257 if (isAutoCorrect && mConfigCandidateHighlightFontColorEnabled) { 258 color = mColorAutoCorrect; 259 } else if (isSuggestedCandidate && mConfigCandidateHighlightFontColorEnabled) { 260 color = mColorSuggestedCandidate; 261 } else { 262 color = mColorTypedWord; 263 } 264 if (info != null && info.isPreviousSuggestedWord()) { 265 final int newAlpha = (int)(Color.alpha(color) * 0.5f); 266 return Color.argb(newAlpha, Color.red(color), Color.green(color), Color.blue(color)); 267 } else { 268 return color; 269 } 270 } 271 272 private void updateSuggestions() { 273 final SuggestedWords suggestions = mSuggestions; 274 final List<SuggestedWordInfo> suggestedWordInfoList = suggestions.mSuggestedWordInfoList; 275 276 clear(); 277 final int paneWidth = getWidth(); 278 final int dividerWidth = mDividers.get(0).getMeasuredWidth(); 279 int x = 0; 280 int y = 0; 281 int fromIndex = NUM_CANDIDATES_IN_STRIP; 282 final int count = Math.min(mWords.size(), suggestions.size()); 283 closeCandidatesPane(); 284 mExpandCandidatesPane.setEnabled(count >= NUM_CANDIDATES_IN_STRIP); 285 for (int i = 0; i < count; i++) { 286 final CharSequence word = suggestions.getWord(i); 287 if (word == null) continue; 288 289 final SuggestedWordInfo info = (suggestedWordInfoList != null) 290 ? suggestedWordInfoList.get(i) : null; 291 final boolean isAutoCorrect = suggestions.mHasMinimalSuggestion 292 && ((i == 1 && !suggestions.mTypedWordValid) 293 || (i == 0 && suggestions.mTypedWordValid)); 294 // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1 295 // and there are multiple suggestions, such as the default punctuation list. 296 // TODO: Need to revisit this logic with bigram suggestions 297 final boolean isSuggestedCandidate = (i != 0); 298 final boolean isPunctuationSuggestions = (word.length() == 1 && count > 1); 299 300 final TextView tv = mWords.get(i); 301 // TODO: Reorder candidates in strip as appropriate. The center candidate should hold 302 // the word when space is typed (valid typed word or auto corrected word). 303 tv.setTextColor(getCandidateTextColor(isAutoCorrect, 304 isSuggestedCandidate || isPunctuationSuggestions, info)); 305 tv.setText(getStyledCandidateWord(word, isAutoCorrect)); 306 // TODO: call TextView.setTextScaleX() to fit the candidate in single line. 307 if (i >= NUM_CANDIDATES_IN_STRIP) { 308 tv.measure(UNSPECIFIED_MEASURESPEC, UNSPECIFIED_MEASURESPEC); 309 final int width = tv.getMeasuredWidth(); 310 // TODO: Handle overflow case. 311 if (dividerWidth + x + width >= paneWidth) { 312 centeringCandidates(fromIndex, i - 1, x, paneWidth); 313 x = 0; 314 y += mCandidateStripHeight; 315 fromIndex = i; 316 } 317 if (x != 0) { 318 final View divider = mDividers.get(i - NUM_CANDIDATES_IN_STRIP); 319 mCandidatesPane.addView(divider); 320 placeCandidateAt(divider, x, y); 321 x += dividerWidth; 322 } 323 mCandidatesPane.addView(tv); 324 placeCandidateAt(tv, x, y); 325 x += width; 326 } 327 328 if (DBG && info != null) { 329 final TextView dv = new TextView(getContext(), null); 330 dv.setTextSize(10.0f); 331 dv.setTextColor(0xff808080); 332 dv.setText(info.getDebugString()); 333 // TODO: debug view for candidate strip needed. 334 mCandidatesPane.addView(dv); 335 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)dv.getLayoutParams(); 336 lp.gravity = Gravity.BOTTOM; 337 } 338 } 339 if (x != 0) { 340 // Centering last candidates row. 341 centeringCandidates(fromIndex, count - 1, x, paneWidth); 342 } 343 } 344 345 private void placeCandidateAt(View v, int x, int y) { 346 ViewGroup.LayoutParams lp = v.getLayoutParams(); 347 if (lp instanceof ViewGroup.MarginLayoutParams) { 348 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp; 349 mlp.width = v.getMeasuredWidth(); 350 mlp.height = v.getMeasuredHeight(); 351 mlp.setMargins(x, y + (mCandidateStripHeight - mlp.height) / 2, 0, 0); 352 } 353 } 354 355 private void centeringCandidates(int from, int to, int width, int paneWidth) { 356 final ViewGroup pane = mCandidatesPane; 357 final int fromIndex = pane.indexOfChild(mWords.get(from)); 358 final int toIndex = pane.indexOfChild(mWords.get(to)); 359 final int offset = (paneWidth - width) / 2; 360 for (int index = fromIndex; index <= toIndex; index++) { 361 offsetMargin(pane.getChildAt(index), offset, 0); 362 } 363 } 364 365 private static void offsetMargin(View v, int dx, int dy) { 366 ViewGroup.LayoutParams lp = v.getLayoutParams(); 367 if (lp instanceof ViewGroup.MarginLayoutParams) { 368 ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp; 369 mlp.setMargins(mlp.leftMargin + dx, mlp.topMargin + dy, 0, 0); 370 } 371 } 372 373 private void expandCandidatesPane() { 374 mExpandCandidatesPane.setVisibility(View.GONE); 375 mCloseCandidatesPane.setVisibility(View.VISIBLE); 376 mCandidatesPaneContainer.setMinimumHeight(mKeyboardView.getMeasuredHeight()); 377 mCandidatesPaneContainer.setVisibility(View.VISIBLE); 378 mKeyboardView.setVisibility(View.GONE); 379 } 380 381 private void closeCandidatesPane() { 382 mExpandCandidatesPane.setVisibility(View.VISIBLE); 383 mCloseCandidatesPane.setVisibility(View.GONE); 384 mCandidatesPaneContainer.setVisibility(View.GONE); 385 mKeyboardView.setVisibility(View.VISIBLE); 386 } 387 388 public void onAutoCorrectionInverted(CharSequence autoCorrectedWord) { 389 // Displaying auto corrected word as inverted is enabled only when highlighting candidate 390 // with color is disabled. 391 if (mConfigCandidateHighlightFontColorEnabled) 392 return; 393 final TextView tv = mWords.get(1); 394 final Spannable word = new SpannableString(autoCorrectedWord); 395 final int wordLength = word.length(); 396 word.setSpan(mInvertedBackgroundColorSpan, 0, wordLength, 397 Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 398 word.setSpan(mInvertedForegroundColorSpan, 0, wordLength, 399 Spanned.SPAN_INCLUSIVE_EXCLUSIVE); 400 tv.setText(word); 401 mShowingAutoCorrectionInverted = true; 402 } 403 404 public boolean isConfigCandidateHighlightFontColorEnabled() { 405 return mConfigCandidateHighlightFontColorEnabled; 406 } 407 408 public boolean isShowingAddToDictionaryHint() { 409 return mShowingAddToDictionary; 410 } 411 412 public void showAddToDictionaryHint(CharSequence word) { 413 SuggestedWords.Builder builder = new SuggestedWords.Builder() 414 .addWord(word) 415 .addWord(getContext().getText(R.string.hint_add_to_dictionary)); 416 setSuggestions(builder.build()); 417 mShowingAddToDictionary = true; 418 // Disable R.string.hint_add_to_dictionary button 419 TextView tv = mWords.get(1); 420 tv.setClickable(false); 421 } 422 423 public boolean dismissAddToDictionaryHint() { 424 if (!mShowingAddToDictionary) return false; 425 clear(); 426 return true; 427 } 428 429 public SuggestedWords getSuggestions() { 430 return mSuggestions; 431 } 432 433 public void clear() { 434 mShowingAddToDictionary = false; 435 mShowingAutoCorrectionInverted = false; 436 for (int i = 0; i < NUM_CANDIDATES_IN_STRIP; i++) 437 mWords.get(i).setText(null); 438 mCandidatesPane.removeAllViews(); 439 } 440 441 private void hidePreview() { 442 mPreviewPopup.dismiss(); 443 } 444 445 private void showPreview(int index, CharSequence word) { 446 if (TextUtils.isEmpty(word)) 447 return; 448 449 final TextView previewText = mPreviewText; 450 previewText.setTextColor(mColorTypedWord); 451 previewText.setText(word); 452 previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 453 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 454 View v = mWords.get(index); 455 final int[] offsetInWindow = new int[2]; 456 v.getLocationInWindow(offsetInWindow); 457 final int posX = offsetInWindow[0]; 458 final int posY = offsetInWindow[1] - previewText.getMeasuredHeight(); 459 final PopupWindow previewPopup = mPreviewPopup; 460 if (previewPopup.isShowing()) { 461 previewPopup.update(posX, posY, previewPopup.getWidth(), previewPopup.getHeight()); 462 } else { 463 previewPopup.showAtLocation(this, Gravity.NO_GRAVITY, posX, posY); 464 } 465 previewText.setVisibility(VISIBLE); 466 mHandler.postHidePreview(); 467 } 468 469 private void addToDictionary(CharSequence word) { 470 if (mListener.addWordToDictionary(word.toString())) { 471 showPreview(0, getContext().getString(R.string.added_word, word)); 472 } 473 } 474 475 @Override 476 public boolean onLongClick(View view) { 477 final Object tag = view.getTag(); 478 if (!(tag instanceof Integer)) 479 return true; 480 final int index = (Integer) tag; 481 if (index >= mSuggestions.size()) 482 return true; 483 484 final CharSequence word = mSuggestions.getWord(index); 485 if (word.length() < 2) 486 return false; 487 addToDictionary(word); 488 return true; 489 } 490 491 @Override 492 public void onClick(View view) { 493 final Object tag = view.getTag(); 494 if (!(tag instanceof Integer)) 495 return; 496 final int index = (Integer) tag; 497 if (index >= mSuggestions.size()) 498 return; 499 500 final CharSequence word = mSuggestions.getWord(index); 501 if (mShowingAddToDictionary && index == 0) { 502 addToDictionary(word); 503 } else { 504 mListener.pickSuggestionManually(index, word); 505 } 506 // Because some punctuation letters are not treated as word separator depending on locale, 507 // {@link #setSuggestions} might not be called and candidates pane left opened. 508 closeCandidatesPane(); 509 } 510 511 @Override 512 public void onDetachedFromWindow() { 513 super.onDetachedFromWindow(); 514 mHandler.cancelAllMessages(); 515 hidePreview(); 516 } 517} 518