2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.inputmethod.latin;
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Canvas;
22 import android.graphics.Paint;
23 import android.graphics.Paint.Align;
24 import android.graphics.Rect;
25 import android.graphics.Typeface;
26 import android.graphics.drawable.Drawable;
27 import android.util.AttributeSet;
28 import android.view.GestureDetector;
29 import android.view.Gravity;
30 import android.view.LayoutInflater;
31 import android.view.MotionEvent;
32 import android.view.View;
33 import android.view.ViewGroup.LayoutParams;
34 import android.widget.PopupWindow;
35 import android.widget.TextView;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.List;
41 public class CandidateView extends View {
43 private static final int OUT_OF_BOUNDS_WORD_INDEX = -1;
44 private static final int OUT_OF_BOUNDS_X_COORD = -1;
46 private LatinIME mService;
47 private final ArrayList<CharSequence> mSuggestions = new ArrayList<CharSequence>();
48 private boolean mShowingCompletions;
49 private CharSequence mSelectedString;
50 private int mSelectedIndex;
51 private int mTouchX = OUT_OF_BOUNDS_X_COORD;
52 private final Drawable mSelectionHighlight;
53 private boolean mTypedWordValid;
55 private boolean mHaveMinimalSuggestion;
57 private Rect mBgPadding;
59 private final TextView mPreviewText;
60 private final PopupWindow mPreviewPopup;
61 private int mCurrentWordIndex;
62 private Drawable mDivider;
64 private static final int MAX_SUGGESTIONS = 32;
65 private static final int SCROLL_PIXELS = 20;
67 private final int[] mWordWidth = new int[MAX_SUGGESTIONS];
68 private final int[] mWordX = new int[MAX_SUGGESTIONS];
69 private int mPopupPreviewX;
70 private int mPopupPreviewY;
72 private static final int X_GAP = 10;
74 private final int mColorNormal;
75 private final int mColorRecommended;
76 private final int mColorOther;
77 private final Paint mPaint;
78 private final int mDescent;
79 private boolean mScrolled;
80 private boolean mShowingAddToDictionary;
81 private CharSequence mAddToDictionaryHint;
83 private int mTargetScrollX;
85 private final int mMinTouchableWidth;
87 private int mTotalWidth;
89 private final GestureDetector mGestureDetector;
92 * Construct a CandidateView for showing suggested words for completion.
96 public CandidateView(Context context, AttributeSet attrs) {
97 super(context, attrs);
98 mSelectionHighlight = context.getResources().getDrawable(
99 R.drawable.list_selector_background_pressed);
101 LayoutInflater inflate =
102 (LayoutInflater) context
103 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
104 Resources res = context.getResources();
105 mPreviewPopup = new PopupWindow(context);
106 mPreviewText = (TextView) inflate.inflate(R.layout.candidate_preview, null);
107 mPreviewPopup.setWindowLayoutMode(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
108 mPreviewPopup.setContentView(mPreviewText);
109 mPreviewPopup.setBackgroundDrawable(null);
110 mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation);
111 mColorNormal = res.getColor(R.color.candidate_normal);
112 mColorRecommended = res.getColor(R.color.candidate_recommended);
113 mColorOther = res.getColor(R.color.candidate_other);
114 mDivider = res.getDrawable(R.drawable.keyboard_suggest_strip_divider);
115 mAddToDictionaryHint = res.getString(R.string.hint_add_to_dictionary);
117 mPaint = new Paint();
118 mPaint.setColor(mColorNormal);
119 mPaint.setAntiAlias(true);
120 mPaint.setTextSize(mPreviewText.getTextSize());
121 mPaint.setStrokeWidth(0);
122 mPaint.setTextAlign(Align.CENTER);
123 mDescent = (int) mPaint.descent();
124 mMinTouchableWidth = (int)res.getDimension(R.dimen.candidate_min_touchable_width);
126 mGestureDetector = new GestureDetector(
127 new CandidateStripGestureListener(mMinTouchableWidth));
128 setWillNotDraw(false);
129 setHorizontalScrollBarEnabled(false);
130 setVerticalScrollBarEnabled(false);
131 scrollTo(0, getScrollY());
134 private class CandidateStripGestureListener extends GestureDetector.SimpleOnGestureListener {
135 private final int mTouchSlopSquare;
137 public CandidateStripGestureListener(int touchSlop) {
138 // Slightly reluctant to scroll to be able to easily choose the suggestion
139 mTouchSlopSquare = touchSlop * touchSlop;
143 public void onLongPress(MotionEvent me) {
144 if (mSuggestions.size() > 0) {
145 if (me.getX() + getScrollX() < mWordWidth[0] && getScrollX() < 10) {
146 longPressFirstWord();
152 public boolean onDown(MotionEvent e) {
158 public boolean onScroll(MotionEvent e1, MotionEvent e2,
159 float distanceX, float distanceY) {
161 // This is applied only when we recognize that scrolling is starting.
162 final int deltaX = (int) (e2.getX() - e1.getX());
163 final int deltaY = (int) (e2.getY() - e1.getY());
164 final int distance = (deltaX * deltaX) + (deltaY * deltaY);
165 if (distance < mTouchSlopSquare) {
171 final int width = getWidth();
173 int scrollX = getScrollX();
174 scrollX += (int) distanceX;
178 if (distanceX > 0 && scrollX + width > mTotalWidth) {
179 scrollX -= (int) distanceX;
181 mTargetScrollX = scrollX;
182 scrollTo(scrollX, getScrollY());
190 * A connection back to the service to communicate with the text field
193 public void setService(LatinIME listener) {
198 public int computeHorizontalScrollRange() {
203 * If the canvas is null, then only touch calculations are performed to pick the target
207 protected void onDraw(Canvas canvas) {
208 if (canvas != null) {
209 super.onDraw(canvas);
213 final int height = getHeight();
214 if (mBgPadding == null) {
215 mBgPadding = new Rect(0, 0, 0, 0);
216 if (getBackground() != null) {
217 getBackground().getPadding(mBgPadding);
219 mDivider.setBounds(0, 0, mDivider.getIntrinsicWidth(),
220 mDivider.getIntrinsicHeight());
223 final int count = mSuggestions.size();
224 final Rect bgPadding = mBgPadding;
225 final Paint paint = mPaint;
226 final int touchX = mTouchX;
227 final int scrollX = getScrollX();
228 final boolean scrolled = mScrolled;
229 final boolean typedWordValid = mTypedWordValid;
230 final int y = (int) (height + mPaint.getTextSize() - mDescent) / 2;
232 boolean existsAutoCompletion = false;
235 for (int i = 0; i < count; i++) {
236 CharSequence suggestion = mSuggestions.get(i);
237 if (suggestion == null) continue;
238 final int wordLength = suggestion.length();
240 paint.setColor(mColorNormal);
241 if (mHaveMinimalSuggestion
242 && ((i == 1 && !typedWordValid) || (i == 0 && typedWordValid))) {
243 paint.setTypeface(Typeface.DEFAULT_BOLD);
244 paint.setColor(mColorRecommended);
245 existsAutoCompletion = true;
246 } else if (i != 0 || (wordLength == 1 && count > 1)) {
247 // HACK: even if i == 0, we use mColorOther when this suggestion's length is 1 and
248 // there are multiple suggestions, such as the default punctuation list.
249 paint.setColor(mColorOther);
252 if ((wordWidth = mWordWidth[i]) == 0) {
253 float textWidth = paint.measureText(suggestion, 0, wordLength);
254 wordWidth = Math.max(mMinTouchableWidth, (int) textWidth + X_GAP * 2);
255 mWordWidth[i] = wordWidth;
260 if (touchX != OUT_OF_BOUNDS_X_COORD && !scrolled
261 && touchX + scrollX >= x && touchX + scrollX < x + wordWidth) {
262 if (canvas != null && !mShowingAddToDictionary) {
263 canvas.translate(x, 0);
264 mSelectionHighlight.setBounds(0, bgPadding.top, wordWidth, height);
265 mSelectionHighlight.draw(canvas);
266 canvas.translate(-x, 0);
268 mSelectedString = suggestion;
272 if (canvas != null) {
273 canvas.drawText(suggestion, 0, wordLength, x + wordWidth / 2, y, paint);
274 paint.setColor(mColorOther);
275 canvas.translate(x + wordWidth, 0);
276 // Draw a divider unless it's after the hint
277 if (!(mShowingAddToDictionary && i == 1)) {
278 mDivider.draw(canvas);
280 canvas.translate(-x - wordWidth, 0);
282 paint.setTypeface(Typeface.DEFAULT);
285 mService.onAutoCompletionStateChanged(existsAutoCompletion);
287 if (mTargetScrollX != scrollX) {
292 private void scrollToTarget() {
293 int scrollX = getScrollX();
294 if (mTargetScrollX > scrollX) {
295 scrollX += SCROLL_PIXELS;
296 if (scrollX >= mTargetScrollX) {
297 scrollX = mTargetScrollX;
298 scrollTo(scrollX, getScrollY());
301 scrollTo(scrollX, getScrollY());
304 scrollX -= SCROLL_PIXELS;
305 if (scrollX <= mTargetScrollX) {
306 scrollX = mTargetScrollX;
307 scrollTo(scrollX, getScrollY());
310 scrollTo(scrollX, getScrollY());
316 public void setSuggestions(List<CharSequence> suggestions, boolean completions,
317 boolean typedWordValid, boolean haveMinimalSuggestion) {
319 if (suggestions != null) {
320 int insertCount = Math.min(suggestions.size(), MAX_SUGGESTIONS);
321 for (CharSequence suggestion : suggestions) {
322 mSuggestions.add(suggestion);
323 if (--insertCount == 0)
327 mShowingCompletions = completions;
328 mTypedWordValid = typedWordValid;
329 scrollTo(0, getScrollY());
331 mHaveMinimalSuggestion = haveMinimalSuggestion;
332 // Compute the total width
338 public boolean isShowingAddToDictionaryHint() {
339 return mShowingAddToDictionary;
342 public void showAddToDictionaryHint(CharSequence word) {
343 ArrayList<CharSequence> suggestions = new ArrayList<CharSequence>();
344 suggestions.add(word);
345 suggestions.add(mAddToDictionaryHint);
346 setSuggestions(suggestions, false, false, false);
347 mShowingAddToDictionary = true;
350 public boolean dismissAddToDictionaryHint() {
351 if (!mShowingAddToDictionary) return false;
356 /* package */ List<CharSequence> getSuggestions() {
360 public void clear() {
361 // Don't call mSuggestions.clear() because it's being used for logging
362 // in LatinIME.pickSuggestionManually().
363 mSuggestions.clear();
364 mTouchX = OUT_OF_BOUNDS_X_COORD;
365 mSelectedString = null;
367 mShowingAddToDictionary = false;
369 Arrays.fill(mWordWidth, 0);
370 Arrays.fill(mWordX, 0);
374 public boolean onTouchEvent(MotionEvent me) {
376 if (mGestureDetector.onTouchEvent(me)) {
380 int action = me.getAction();
381 int x = (int) me.getX();
382 int y = (int) me.getY();
386 case MotionEvent.ACTION_DOWN:
389 case MotionEvent.ACTION_MOVE:
392 if (mSelectedString != null) {
393 // If there are completions from the application, we don't change the state to
394 // STATE_PICKED_SUGGESTION
395 if (!mShowingCompletions) {
396 // This "acceptedSuggestion" will not be counted as a word because
397 // it will be counted in pickSuggestion instead.
398 TextEntryState.acceptedSuggestion(mSuggestions.get(0),
401 mService.pickSuggestionManually(mSelectedIndex, mSelectedString);
402 mSelectedString = null;
407 case MotionEvent.ACTION_UP:
409 if (mSelectedString != null) {
410 if (mShowingAddToDictionary) {
411 longPressFirstWord();
414 if (!mShowingCompletions) {
415 TextEntryState.acceptedSuggestion(mSuggestions.get(0),
418 mService.pickSuggestionManually(mSelectedIndex, mSelectedString);
422 mSelectedString = null;
432 private void hidePreview() {
433 mTouchX = OUT_OF_BOUNDS_X_COORD;
434 mCurrentWordIndex = OUT_OF_BOUNDS_WORD_INDEX;
435 mPreviewPopup.dismiss();
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_WORD_INDEX) {
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() - getScrollX()
456 + (mWordWidth[wordIndex] - wordWidth) / 2;
457 mPopupPreviewY = - popupHeight;
458 int [] offsetInWindow = new int[2];
459 getLocationInWindow(offsetInWindow);
460 if (mPreviewPopup.isShowing()) {
461 mPreviewPopup.update(mPopupPreviewX, mPopupPreviewY + offsetInWindow[1],
462 popupWidth, popupHeight);
464 mPreviewPopup.setWidth(popupWidth);
465 mPreviewPopup.setHeight(popupHeight);
466 mPreviewPopup.showAtLocation(this, Gravity.NO_GRAVITY, mPopupPreviewX,
467 mPopupPreviewY + offsetInWindow[1]);
469 mPreviewText.setVisibility(VISIBLE);
474 private void longPressFirstWord() {
475 CharSequence word = mSuggestions.get(0);
476 if (word.length() < 2) return;
477 if (mService.addWordToDictionary(word.toString())) {
478 showPreview(0, getContext().getResources().getString(R.string.added_word, word));
483 public void onDetachedFromWindow() {
484 super.onDetachedFromWindow();