• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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