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