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