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