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