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