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