CandidateView.java revision 41feaaadb758a8b31d3e436063b4b5faed104d4d
1/* 2 * Copyright (C) 2008 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 java.util.ArrayList; 20import java.util.Arrays; 21import java.util.List; 22 23import android.content.Context; 24import android.content.res.Resources; 25import android.graphics.Canvas; 26import android.graphics.Paint; 27import android.graphics.Rect; 28import android.graphics.Typeface; 29import android.graphics.Paint.Align; 30import android.graphics.drawable.Drawable; 31import android.os.Handler; 32import android.os.Message; 33import android.util.AttributeSet; 34import android.view.GestureDetector; 35import android.view.Gravity; 36import android.view.LayoutInflater; 37import android.view.MotionEvent; 38import android.view.View; 39import android.view.ViewGroup.LayoutParams; 40import android.widget.PopupWindow; 41import android.widget.TextView; 42 43public class CandidateView extends View { 44 45 private static final int OUT_OF_BOUNDS = -1; 46 private static final List<CharSequence> EMPTY_LIST = new ArrayList<CharSequence>(); 47 48 private LatinIME mService; 49 private List<CharSequence> mSuggestions = EMPTY_LIST; 50 private boolean mShowingCompletions; 51 private CharSequence mSelectedString; 52 private int mSelectedIndex; 53 private int mTouchX = OUT_OF_BOUNDS; 54 private Drawable mSelectionHighlight; 55 private boolean mTypedWordValid; 56 57 private boolean mHaveMinimalSuggestion; 58 59 private Rect mBgPadding; 60 61 private TextView mPreviewText; 62 private PopupWindow mPreviewPopup; 63 private int mCurrentWordIndex; 64 private Drawable mDivider; 65 66 private static final int MAX_SUGGESTIONS = 32; 67 private static final int SCROLL_PIXELS = 20; 68 69 private static final int MSG_REMOVE_PREVIEW = 1; 70 private static final int MSG_REMOVE_THROUGH_PREVIEW = 2; 71 72 private int[] mWordWidth = new int[MAX_SUGGESTIONS]; 73 private int[] mWordX = new int[MAX_SUGGESTIONS]; 74 private int mPopupPreviewX; 75 private int mPopupPreviewY; 76 77 private static final int X_GAP = 10; 78 79 private int mColorNormal; 80 private int mColorRecommended; 81 private int mColorOther; 82 private Paint mPaint; 83 private int mDescent; 84 private boolean mScrolled; 85 private boolean mShowingAddToDictionary; 86 private CharSequence mAddToDictionaryHint; 87 88 private int mTargetScrollX; 89 90 private int mMinTouchableWidth; 91 92 private int mTotalWidth; 93 94 private GestureDetector mGestureDetector; 95 96 Handler mHandler = new Handler() { 97 @Override 98 public void handleMessage(Message msg) { 99 switch (msg.what) { 100 case MSG_REMOVE_PREVIEW: 101 mPreviewText.setVisibility(GONE); 102 break; 103 case MSG_REMOVE_THROUGH_PREVIEW: 104 mPreviewText.setVisibility(GONE); 105 if (mTouchX != OUT_OF_BOUNDS) { 106 removeHighlight(); 107 } 108 break; 109 } 110 } 111 }; 112 113 /** 114 * Construct a CandidateView for showing suggested words for completion. 115 * @param context 116 * @param attrs 117 */ 118 public CandidateView(Context context, AttributeSet attrs) { 119 super(context, attrs); 120 mSelectionHighlight = context.getResources().getDrawable( 121 R.drawable.list_selector_background_pressed); 122 123 LayoutInflater inflate = 124 (LayoutInflater) context 125 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 126 Resources res = context.getResources(); 127 mPreviewPopup = new PopupWindow(context); 128 mPreviewText = (TextView) inflate.inflate(R.layout.candidate_preview, null); 129 mPreviewPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); 130 mPreviewPopup.setContentView(mPreviewText); 131 mPreviewPopup.setBackgroundDrawable(null); 132 mColorNormal = res.getColor(R.color.candidate_normal); 133 mColorRecommended = res.getColor(R.color.candidate_recommended); 134 mColorOther = res.getColor(R.color.candidate_other); 135 mDivider = res.getDrawable(R.drawable.keyboard_suggest_strip_divider); 136 mAddToDictionaryHint = res.getString(R.string.hint_add_to_dictionary); 137 138 mPaint = new Paint(); 139 mPaint.setColor(mColorNormal); 140 mPaint.setAntiAlias(true); 141 mPaint.setTextSize(mPreviewText.getTextSize()); 142 mPaint.setStrokeWidth(0); 143 mPaint.setTextAlign(Align.CENTER); 144 mDescent = (int) mPaint.descent(); 145 // 50 pixels for a 160dpi device would mean about 0.3 inch 146 mMinTouchableWidth = (int) (getResources().getDisplayMetrics().density * 50); 147 148 // Slightly reluctant to scroll to be able to easily choose the suggestion 149 // 50 pixels for a 160dpi device would mean about 0.3 inch 150 final int touchSlop = (int) (getResources().getDisplayMetrics().density * 50); 151 final int touchSlopSquare = touchSlop * touchSlop; 152 mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { 153 @Override 154 public void onLongPress(MotionEvent me) { 155 if (mSuggestions.size() > 0) { 156 if (me.getX() + getScrollX() < mWordWidth[0] && getScrollX() < 10) { 157 longPressFirstWord(); 158 } 159 } 160 } 161 162 @Override 163 public boolean onScroll(MotionEvent e1, MotionEvent e2, 164 float distanceX, float distanceY) { 165 final int deltaX = (int) (e2.getX() - e1.getX()); 166 final int deltaY = (int) (e2.getY() - e1.getY()); 167 final int distance = (deltaX * deltaX) + (deltaY * deltaY); 168 if (distance < touchSlopSquare) { 169 return false; 170 } 171 172 final int width = getWidth(); 173 mScrolled = true; 174 int scrollX = getScrollX(); 175 scrollX += (int) distanceX; 176 if (scrollX < 0) { 177 scrollX = 0; 178 } 179 if (distanceX > 0 && scrollX + width > mTotalWidth) { 180 scrollX -= (int) distanceX; 181 } 182 mTargetScrollX = scrollX; 183 scrollTo(scrollX, getScrollY()); 184 hidePreview(); 185 invalidate(); 186 return true; 187 } 188 }); 189 setHorizontalFadingEdgeEnabled(true); 190 setWillNotDraw(false); 191 setHorizontalScrollBarEnabled(false); 192 setVerticalScrollBarEnabled(false); 193 scrollTo(0, getScrollY()); 194 } 195 196 /** 197 * A connection back to the service to communicate with the text field 198 * @param listener 199 */ 200 public void setService(LatinIME listener) { 201 mService = listener; 202 } 203 204 @Override 205 public int computeHorizontalScrollRange() { 206 return mTotalWidth; 207 } 208 209 /** 210 * If the canvas is null, then only touch calculations are performed to pick the target 211 * candidate. 212 */ 213 @Override 214 protected void onDraw(Canvas canvas) { 215 if (canvas != null) { 216 super.onDraw(canvas); 217 } 218 mTotalWidth = 0; 219 if (mSuggestions == null) return; 220 221 final int height = getHeight(); 222 if (mBgPadding == null) { 223 mBgPadding = new Rect(0, 0, 0, 0); 224 if (getBackground() != null) { 225 getBackground().getPadding(mBgPadding); 226 } 227 mDivider.setBounds(0, 0, mDivider.getIntrinsicWidth(), 228 mDivider.getIntrinsicHeight()); 229 } 230 int x = 0; 231 final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); 232 final Rect bgPadding = mBgPadding; 233 final Paint paint = mPaint; 234 final int touchX = mTouchX; 235 final int scrollX = getScrollX(); 236 final boolean scrolled = mScrolled; 237 final boolean typedWordValid = mTypedWordValid; 238 final int y = (int) (height + mPaint.getTextSize() - mDescent) / 2; 239 240 boolean existsAutoCompletion = false; 241 242 for (int i = 0; i < count; i++) { 243 CharSequence suggestion = mSuggestions.get(i); 244 if (suggestion == null) continue; 245 paint.setColor(mColorNormal); 246 if (mHaveMinimalSuggestion 247 && ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid))) { 248 paint.setTypeface(Typeface.DEFAULT_BOLD); 249 paint.setColor(mColorRecommended); 250 existsAutoCompletion = true; 251 } else if (i != 0) { 252 paint.setColor(mColorOther); 253 } 254 final int wordWidth; 255 if (mWordWidth[i] != 0) { 256 wordWidth = mWordWidth[i]; 257 } else { 258 float textWidth = paint.measureText(suggestion, 0, suggestion.length()); 259 wordWidth = Math.max(mMinTouchableWidth, (int) textWidth + X_GAP * 2); 260 mWordWidth[i] = wordWidth; 261 } 262 263 mWordX[i] = x; 264 265 if (touchX + scrollX >= x && touchX + scrollX < x + wordWidth && !scrolled && 266 touchX != OUT_OF_BOUNDS) { 267 if (canvas != null && !mShowingAddToDictionary) { 268 canvas.translate(x, 0); 269 mSelectionHighlight.setBounds(0, bgPadding.top, wordWidth, height); 270 mSelectionHighlight.draw(canvas); 271 canvas.translate(-x, 0); 272 showPreview(i, null); 273 } 274 mSelectedString = suggestion; 275 mSelectedIndex = i; 276 } 277 278 if (canvas != null) { 279 canvas.drawText(suggestion, 0, suggestion.length(), x + wordWidth / 2, y, paint); 280 paint.setColor(mColorOther); 281 canvas.translate(x + wordWidth, 0); 282 // Draw a divider unless it's after the hint 283 if (!(mShowingAddToDictionary && i == 1)) { 284 mDivider.draw(canvas); 285 } 286 canvas.translate(-x - wordWidth, 0); 287 } 288 paint.setTypeface(Typeface.DEFAULT); 289 x += wordWidth; 290 } 291 mService.onAutoCompletionStateChanged(existsAutoCompletion); 292 mTotalWidth = x; 293 if (mTargetScrollX != scrollX) { 294 scrollToTarget(); 295 } 296 } 297 298 private void scrollToTarget() { 299 int scrollX = getScrollX(); 300 if (mTargetScrollX > scrollX) { 301 scrollX += SCROLL_PIXELS; 302 if (scrollX >= mTargetScrollX) { 303 scrollX = mTargetScrollX; 304 scrollTo(scrollX, getScrollY()); 305 requestLayout(); 306 } else { 307 scrollTo(scrollX, getScrollY()); 308 } 309 } else { 310 scrollX -= SCROLL_PIXELS; 311 if (scrollX <= mTargetScrollX) { 312 scrollX = mTargetScrollX; 313 scrollTo(scrollX, getScrollY()); 314 requestLayout(); 315 } else { 316 scrollTo(scrollX, getScrollY()); 317 } 318 } 319 invalidate(); 320 } 321 322 public void setSuggestions(List<CharSequence> suggestions, boolean completions, 323 boolean typedWordValid, boolean haveMinimalSuggestion) { 324 clear(); 325 if (suggestions != null) { 326 mSuggestions = new ArrayList<CharSequence>(suggestions); 327 } 328 mShowingCompletions = completions; 329 mTypedWordValid = typedWordValid; 330 scrollTo(0, getScrollY()); 331 mTargetScrollX = 0; 332 mHaveMinimalSuggestion = haveMinimalSuggestion; 333 // Compute the total width 334 onDraw(null); 335 invalidate(); 336 requestLayout(); 337 } 338 339 public boolean isShowingAddToDictionaryHint() { 340 return mShowingAddToDictionary; 341 } 342 343 public void showAddToDictionaryHint(CharSequence word) { 344 ArrayList<CharSequence> suggestions = new ArrayList<CharSequence>(); 345 suggestions.add(word); 346 suggestions.add(mAddToDictionaryHint); 347 setSuggestions(suggestions, false, false, false); 348 mShowingAddToDictionary = true; 349 } 350 351 public boolean dismissAddToDictionaryHint() { 352 if (!mShowingAddToDictionary) return false; 353 clear(); 354 return true; 355 } 356 357 public void scrollPrev() { 358 int i = 0; 359 final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); 360 int firstItem = 0; // Actually just before the first item, if at the boundary 361 while (i < count) { 362 if (mWordX[i] < getScrollX() 363 && mWordX[i] + mWordWidth[i] >= getScrollX() - 1) { 364 firstItem = i; 365 break; 366 } 367 i++; 368 } 369 int leftEdge = mWordX[firstItem] + mWordWidth[firstItem] - getWidth(); 370 if (leftEdge < 0) leftEdge = 0; 371 updateScrollPosition(leftEdge); 372 } 373 374 public void scrollNext() { 375 int i = 0; 376 int scrollX = getScrollX(); 377 int targetX = scrollX; 378 final int count = Math.min(mSuggestions.size(), MAX_SUGGESTIONS); 379 int rightEdge = scrollX + getWidth(); 380 while (i < count) { 381 if (mWordX[i] <= rightEdge && 382 mWordX[i] + mWordWidth[i] >= rightEdge) { 383 targetX = Math.min(mWordX[i], mTotalWidth - getWidth()); 384 break; 385 } 386 i++; 387 } 388 updateScrollPosition(targetX); 389 } 390 391 private void updateScrollPosition(int targetX) { 392 if (targetX != getScrollX()) { 393 // TODO: Animate 394 mTargetScrollX = targetX; 395 requestLayout(); 396 invalidate(); 397 mScrolled = true; 398 } 399 } 400 401 /* package */ List<CharSequence> getSuggestions() { 402 return mSuggestions; 403 } 404 405 public void clear() { 406 // Don't call mSuggestions.clear() because it's being used for logging 407 // in LatinIME.pickSuggestionManually(). 408 mSuggestions = EMPTY_LIST; 409 mTouchX = OUT_OF_BOUNDS; 410 mSelectedString = null; 411 mSelectedIndex = -1; 412 mShowingAddToDictionary = false; 413 invalidate(); 414 Arrays.fill(mWordWidth, 0); 415 Arrays.fill(mWordX, 0); 416 if (mPreviewPopup.isShowing()) { 417 mPreviewPopup.dismiss(); 418 } 419 } 420 421 @Override 422 public boolean onTouchEvent(MotionEvent me) { 423 424 if (mGestureDetector.onTouchEvent(me)) { 425 return true; 426 } 427 428 int action = me.getAction(); 429 int x = (int) me.getX(); 430 int y = (int) me.getY(); 431 mTouchX = x; 432 433 switch (action) { 434 case MotionEvent.ACTION_DOWN: 435 mScrolled = false; 436 invalidate(); 437 break; 438 case MotionEvent.ACTION_MOVE: 439 if (y <= 0) { 440 // Fling up!? 441 if (mSelectedString != null) { 442 // If there are completions from the application, we don't change the state to 443 // STATE_PICKED_SUGGESTION 444 if (!mShowingCompletions) { 445 // This "acceptedSuggestion" will not be counted as a word because 446 // it will be counted in pickSuggestion instead. 447 TextEntryState.acceptedSuggestion(mSuggestions.get(0), 448 mSelectedString); 449 } 450 mService.pickSuggestionManually(mSelectedIndex, mSelectedString); 451 mSelectedString = null; 452 mSelectedIndex = -1; 453 } 454 } 455 invalidate(); 456 break; 457 case MotionEvent.ACTION_UP: 458 if (!mScrolled) { 459 if (mSelectedString != null) { 460 if (mShowingAddToDictionary) { 461 longPressFirstWord(); 462 clear(); 463 } else { 464 if (!mShowingCompletions) { 465 TextEntryState.acceptedSuggestion(mSuggestions.get(0), 466 mSelectedString); 467 } 468 mService.pickSuggestionManually(mSelectedIndex, mSelectedString); 469 } 470 } 471 } 472 mSelectedString = null; 473 mSelectedIndex = -1; 474 removeHighlight(); 475 hidePreview(); 476 requestLayout(); 477 break; 478 } 479 return true; 480 } 481 482 private void hidePreview() { 483 mCurrentWordIndex = OUT_OF_BOUNDS; 484 if (mPreviewPopup.isShowing()) { 485 mHandler.sendMessageDelayed(mHandler 486 .obtainMessage(MSG_REMOVE_PREVIEW), 60); 487 } 488 } 489 490 private void showPreview(int wordIndex, String altText) { 491 int oldWordIndex = mCurrentWordIndex; 492 mCurrentWordIndex = wordIndex; 493 // If index changed or changing text 494 if (oldWordIndex != mCurrentWordIndex || altText != null) { 495 if (wordIndex == OUT_OF_BOUNDS) { 496 hidePreview(); 497 } else { 498 CharSequence word = altText != null? altText : mSuggestions.get(wordIndex); 499 mPreviewText.setText(word); 500 mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 501 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 502 int wordWidth = (int) (mPaint.measureText(word, 0, word.length()) + X_GAP * 2); 503 final int popupWidth = wordWidth 504 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight(); 505 final int popupHeight = mPreviewText.getMeasuredHeight(); 506 //mPreviewText.setVisibility(INVISIBLE); 507 mPopupPreviewX = mWordX[wordIndex] - mPreviewText.getPaddingLeft() - getScrollX() 508 + (mWordWidth[wordIndex] - wordWidth) / 2; 509 mPopupPreviewY = - popupHeight; 510 mHandler.removeMessages(MSG_REMOVE_PREVIEW); 511 int [] offsetInWindow = new int[2]; 512 getLocationInWindow(offsetInWindow); 513 if (mPreviewPopup.isShowing()) { 514 mPreviewPopup.update(mPopupPreviewX, mPopupPreviewY + offsetInWindow[1], 515 popupWidth, popupHeight); 516 } else { 517 mPreviewPopup.setWidth(popupWidth); 518 mPreviewPopup.setHeight(popupHeight); 519 mPreviewPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupPreviewX, 520 mPopupPreviewY + offsetInWindow[1]); 521 } 522 mPreviewText.setVisibility(VISIBLE); 523 } 524 } 525 } 526 527 private void removeHighlight() { 528 mTouchX = OUT_OF_BOUNDS; 529 invalidate(); 530 } 531 532 private void longPressFirstWord() { 533 CharSequence word = mSuggestions.get(0); 534 if (word.length() < 2) return; 535 if (mService.addWordToDictionary(word.toString())) { 536 showPreview(0, getContext().getResources().getString(R.string.added_word, word)); 537 } 538 } 539 540 @Override 541 public void onDetachedFromWindow() { 542 super.onDetachedFromWindow(); 543 hidePreview(); 544 } 545} 546