PageRenderTime 40ms CodeModel.GetById 11ms app.highlight 24ms RepoModel.GetById 1ms app.codeStats 0ms

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