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