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