/ime/latinime/src/com/googlecode/eyesfree/inputmethod/latin/CandidateView.java

http://eyes-free.googlecode.com/ · Java · 487 lines · 391 code · 53 blank · 43 comment · 83 complexity · 8a0f7cccf6dc2f451cc97be35b5ec3ec MD5 · raw file

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