Home | History | Annotate | Download | only in openwnn
      1 /*
      2  * Copyright (C) 2008,2009  OMRON SOFTWARE Co., Ltd.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of 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,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package jp.co.omronsoft.openwnn;
     18 
     19 import java.util.ArrayList;
     20 import android.content.Context;
     21 import android.content.SharedPreferences;
     22 import android.content.res.Configuration;
     23 import android.content.res.Resources;
     24 import android.media.MediaPlayer;
     25 import android.os.Vibrator;
     26 import android.text.TextUtils;
     27 import android.text.TextPaint;
     28 import android.text.SpannableString;
     29 import android.text.Spanned;
     30 import android.text.style.ImageSpan;
     31 import android.text.style.DynamicDrawableSpan;
     32 import android.util.Log;
     33 import android.util.DisplayMetrics;
     34 import android.view.Gravity;
     35 import android.view.MotionEvent;
     36 import android.view.View;
     37 import android.view.ViewGroup;
     38 import android.view.View.OnClickListener;
     39 import android.view.View.OnLongClickListener;
     40 import android.view.View.OnTouchListener;
     41 import android.view.GestureDetector;
     42 import android.view.LayoutInflater;
     43 import android.widget.Button;
     44 import android.widget.LinearLayout;
     45 import android.widget.ScrollView;
     46 import android.widget.TextView;
     47 import android.widget.EditText;
     48 import android.widget.AbsoluteLayout;
     49 import android.widget.ImageView;
     50 import android.graphics.drawable.Drawable;
     51 
     52 /**
     53  * The default candidates view manager class using {@link EditText}.
     54  *
     55  * @author Copyright (C) 2009 OMRON SOFTWARE CO., LTD.  All Rights Reserved.
     56  */
     57 public class TextCandidatesViewManager implements CandidatesViewManager, GestureDetector.OnGestureListener {
     58     /** Height of a line */
     59     public static final int LINE_HEIGHT = 34;
     60     /** Number of lines to display (Portrait) */
     61     public static final int LINE_NUM_PORTRAIT       = 2;
     62     /** Number of lines to display (Landscape) */
     63     public static final int LINE_NUM_LANDSCAPE      = 1;
     64 
     65     /** Maximum lines */
     66     private static final int DISPLAY_LINE_MAX_COUNT = 1000;
     67     /** Width of the view */
     68     private static final int CANDIDATE_MINIMUM_WIDTH = 48;
     69     /** Height of the view */
     70     private static final int CANDIDATE_MINIMUM_HEIGHT = 35;
     71     /** Align the candidate left if the width of the string exceeds this threshold */
     72     private static final int CANDIDATE_LEFT_ALIGN_THRESHOLD = 120;
     73     /** Maximum number of displaying candidates par one line (full view mode) */
     74     private static final int FULL_VIEW_DIV = 4;
     75 
     76     /** Body view of the candidates list */
     77     private ViewGroup  mViewBody;
     78     /** Scroller of {@code mViewBodyText} */
     79     private ScrollView mViewBodyScroll;
     80     /** Base of {@code mViewCandidateList1st}, {@code mViewCandidateList2nd} */
     81     private ViewGroup mViewCandidateBase;
     82     /** Button displayed bottom of the view when there are more candidates. */
     83     private ImageView mReadMoreButton;
     84     /** The view of the scaling up candidate */
     85     private View mViewScaleUp;
     86     /** Layout for the candidates list on normal view */
     87     private LinearLayout mViewCandidateList1st;
     88     /** Layout for the candidates list on full view */
     89     private AbsoluteLayout mViewCandidateList2nd;
     90     /** {@link OpenWnn} instance using this manager */
     91     private OpenWnn mWnn;
     92     /** View type (VIEW_TYPE_NORMAL or VIEW_TYPE_FULL or VIEW_TYPE_CLOSE) */
     93     private int mViewType;
     94     /** Portrait display({@code true}) or landscape({@code false}) */
     95     private boolean mPortrait;
     96 
     97     /** Width of the view */
     98     private int mViewWidth;
     99     /** Height of the view */
    100     private int mViewHeight;
    101     /** Minimum width of a candidate (density support) */
    102     private int mCandidateMinimumWidth;
    103     /** Maximum width of a candidate (density support) */
    104     private int mCandidateMinimumHeight;
    105 
    106     /** Whether hide the view if there is no candidates */
    107     private boolean mAutoHideMode;
    108     /** The converter to be get candidates from and notice the selected candidate to. */
    109     private WnnEngine mConverter;
    110     /** Limitation of displaying candidates */
    111     private int mDisplayLimit;
    112 
    113     /** Vibrator for touch vibration */
    114     private Vibrator mVibrator = null;
    115     /** MediaPlayer for click sound */
    116     private MediaPlayer mSound = null;
    117 
    118     /** Number of candidates displaying */
    119     private int mWordCount;
    120     /** List of candidates */
    121     private ArrayList<WnnWord> mWnnWordArray;
    122 
    123     /** Gesture detector */
    124     private GestureDetector mGestureDetector;
    125     /** The word pressed */
    126     private WnnWord mWord;
    127     /** Character width of the candidate area */
    128     private int mLineLength = 0;
    129     /** Number of lines displayed */
    130     private int mLineCount = 1;
    131 
    132     /** {@code true} if the candidate delete state is selected */
    133     private boolean mIsScaleUp = false;
    134 
    135     /** {@code true} if the full screen mode is selected */
    136     private boolean mIsFullView = false;
    137 
    138     /** The event object for "touch" */
    139     private MotionEvent mMotionEvent = null;
    140 
    141     /** The offset when the candidates is flowed out the candidate window */
    142     private int mDisplayEndOffset = 0;
    143     /** {@code true} if there are more candidates to display. */
    144     private boolean mCanReadMore = false;
    145     /** Width of {@code mReadMoreButton} */
    146     private int mReadMoreButtonWidth = 0;
    147     /** Color of the candidates */
    148     private int mTextColor = 0;
    149     /** Template object for each candidate and normal/full view change button */
    150     private TextView mViewCandidateTemplate;
    151     /** Number of candidates in full view */
    152     private int mFullViewWordCount;
    153     /** Number of candidates in the current line (in full view) */
    154     private int mFullViewOccupyCount;
    155     /** View of the previous candidate (in full view) */
    156     private TextView mFullViewPrevView;
    157     /** Id of the top line view (in full view) */
    158     private int mFullViewPrevLineTopId;
    159     /** Layout of the previous candidate (in full view) */
    160     private ViewGroup.LayoutParams mFullViewPrevParams;
    161     /** Whether all candidates is displayed */
    162     private boolean mCreateCandidateDone;
    163     /** Number of lines in normal view */
    164     private int mNormalViewWordCountOfLine;
    165     /** general infomation about a display */
    166     private final DisplayMetrics mMetrics = new DisplayMetrics();
    167 
    168     /** Event listener for touching a candidate */
    169     private OnTouchListener mCandidateOnTouch = new OnTouchListener() {
    170             public boolean onTouch(View v, MotionEvent event) {
    171                 if (mMotionEvent != null) {
    172                     return true;
    173                 }
    174 
    175                 if ((event.getAction() == MotionEvent.ACTION_UP)
    176                     && (v instanceof TextView)) {
    177                     Drawable d = v.getBackground();
    178                     if (d != null) {
    179                         d.setState(new int[] {});
    180                     }
    181                 }
    182 
    183                 mMotionEvent = event;
    184                 boolean ret = mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.CANDIDATE_VIEW_TOUCH));
    185                 mMotionEvent = null;
    186                 return ret;
    187             }
    188         };
    189 
    190 
    191     /** Event listener for clicking a candidate */
    192     private OnClickListener mCandidateOnClick = new OnClickListener() {
    193             public void onClick(View v) {
    194                 if (!v.isShown()) {
    195                     return;
    196                 }
    197 
    198                 if (v instanceof TextView) {
    199                     TextView text = (TextView)v;
    200                     int wordcount = text.getId();
    201                     WnnWord word = null;
    202                     word = mWnnWordArray.get(wordcount);
    203                     selectCandidate(word);
    204                 }
    205             }
    206         };
    207 
    208     /** Event listener for long-clicking a candidate */
    209     private OnLongClickListener mCandidateOnLongClick = new OnLongClickListener() {
    210             public boolean onLongClick(View v) {
    211                 if (mViewScaleUp == null) {
    212                     return false;
    213                 }
    214 
    215                 if (!v.isShown()) {
    216                     return true;
    217                 }
    218 
    219                 Drawable d = v.getBackground();
    220                 if (d != null) {
    221                     if(d.getState().length == 0){
    222                         return true;
    223                     }
    224                 }
    225 
    226                 int wordcount = ((TextView)v).getId();
    227                 mWord = mWnnWordArray.get(wordcount);
    228                 setViewScaleUp(true, mWord);
    229 
    230                 return true;
    231             }
    232         };
    233 
    234 
    235     /**
    236      * Constructor
    237      */
    238     public TextCandidatesViewManager() {
    239         this(-1);
    240     }
    241 
    242     /**
    243      * Constructor
    244      *
    245      * @param displayLimit      The limit of display
    246      */
    247     public TextCandidatesViewManager(int displayLimit) {
    248         this.mDisplayLimit = displayLimit;
    249         this.mWnnWordArray = new ArrayList<WnnWord>();
    250         this.mAutoHideMode = true;
    251         mMetrics.setToDefaults();
    252     }
    253 
    254     /**
    255      * Set auto-hide mode.
    256      * @param hide      {@code true} if the view will hidden when no candidate exists;
    257      *                  {@code false} if the view is always shown.
    258      */
    259     public void setAutoHide(boolean hide) {
    260         mAutoHideMode = hide;
    261     }
    262 
    263     /** @see CandidatesViewManager */
    264     public View initView(OpenWnn parent, int width, int height) {
    265         mWnn = parent;
    266         mViewWidth = width;
    267         mViewHeight = height;
    268         mCandidateMinimumWidth = (int)(CANDIDATE_MINIMUM_WIDTH * mMetrics.density);
    269         mCandidateMinimumHeight = (int)(CANDIDATE_MINIMUM_HEIGHT * mMetrics.density);
    270         mPortrait =
    271             (parent.getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE);
    272 
    273         Resources r = mWnn.getResources();
    274 
    275         LayoutInflater inflater = parent.getLayoutInflater();
    276         mViewBody = (ViewGroup)inflater.inflate(R.layout.candidates, null);
    277 
    278         mViewBodyScroll = (ScrollView)mViewBody.findViewById(R.id.candview_scroll);
    279         mViewBodyScroll.setOnTouchListener(mCandidateOnTouch);
    280 
    281         mViewCandidateBase = (ViewGroup)mViewBody.findViewById(R.id.candview_base);
    282 
    283         createNormalCandidateView();
    284         mViewCandidateList2nd = (AbsoluteLayout)mViewBody.findViewById(R.id.candidates_2nd_view);
    285 
    286         mReadMoreButtonWidth = r.getDrawable(R.drawable.cand_up).getMinimumWidth();
    287 
    288         mTextColor = r.getColor(R.color.candidate_text);
    289 
    290         mReadMoreButton = (ImageView)mViewBody.findViewById(R.id.read_more_text);
    291         mReadMoreButton.setOnTouchListener(new View.OnTouchListener() {
    292                 public boolean onTouch(View v, MotionEvent event) {
    293                     switch (event.getAction()) {
    294                     case MotionEvent.ACTION_DOWN:
    295                         if (mIsFullView) {
    296                             mReadMoreButton.setImageResource(R.drawable.cand_down_press);
    297                         } else {
    298                             mReadMoreButton.setImageResource(R.drawable.cand_up_press);
    299                         }
    300                 	    break;
    301                     case MotionEvent.ACTION_UP:
    302                         if (mIsFullView) {
    303                             mReadMoreButton.setImageResource(R.drawable.cand_down);
    304                         } else {
    305                             mReadMoreButton.setImageResource(R.drawable.cand_up);
    306                         }
    307                         break;
    308                     default:
    309                         break;
    310                     }
    311                     return false;
    312                 }
    313             });
    314         mReadMoreButton.setOnClickListener(new View.OnClickListener() {
    315                 public void onClick(View v) {
    316                     if (!v.isShown()) {
    317                         return;
    318                     }
    319 
    320                     if (mIsFullView) {
    321                         mIsFullView = false;
    322                         mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.LIST_CANDIDATES_NORMAL));
    323                     } else {
    324                         mIsFullView = true;
    325                         mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.LIST_CANDIDATES_FULL));
    326                     }
    327                 }
    328             });
    329 
    330         setViewType(CandidatesViewManager.VIEW_TYPE_CLOSE);
    331 
    332         mGestureDetector = new GestureDetector(this);
    333 
    334         View scaleUp = (View)inflater.inflate(R.layout.candidate_scale_up, null);
    335         mViewScaleUp = scaleUp;
    336 
    337         /* select button */
    338         Button b = (Button)scaleUp.findViewById(R.id.candidate_select);
    339         b.setOnClickListener(new View.OnClickListener() {
    340                 public void onClick(View v) {
    341                     selectCandidate(mWord);
    342                 }
    343             });
    344 
    345         /* cancel button */
    346         b = (Button)scaleUp.findViewById(R.id.candidate_cancel);
    347         b.setOnClickListener(new View.OnClickListener() {
    348                 public void onClick(View v) {
    349                     setViewLayout(CandidatesViewManager.VIEW_TYPE_NORMAL);
    350                     mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.UPDATE_CANDIDATE));
    351                 }
    352             });
    353 
    354         return mViewBody;
    355     }
    356 
    357     /**
    358      * Create the normal candidate view
    359      */
    360     private void createNormalCandidateView() {
    361         mViewCandidateList1st = (LinearLayout)mViewBody.findViewById(R.id.candidates_1st_view);
    362         mViewCandidateList1st.setOnTouchListener(mCandidateOnTouch);
    363         mViewCandidateList1st.setOnClickListener(mCandidateOnClick);
    364 
    365         int line = getMaxLine();
    366         int width = mViewWidth;
    367         for (int i = 0; i < line; i++) {
    368             LinearLayout lineView = new LinearLayout(mViewBodyScroll.getContext());
    369             lineView.setOrientation(LinearLayout.HORIZONTAL);
    370             LinearLayout.LayoutParams layoutParams =
    371                 new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
    372                                               ViewGroup.LayoutParams.WRAP_CONTENT);
    373             lineView.setLayoutParams(layoutParams);
    374             for (int j = 0; j < (width / getCandidateMinimumWidth()); j++) {
    375                 TextView tv = createCandidateView();
    376                 lineView.addView(tv);
    377             }
    378 
    379             if (i == 0) {
    380                 TextView tv = createCandidateView();
    381                 layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
    382                                                              ViewGroup.LayoutParams.WRAP_CONTENT);
    383                 layoutParams.weight = 0;
    384                 layoutParams.gravity = Gravity.RIGHT;
    385                 tv.setLayoutParams(layoutParams);
    386 
    387                 lineView.addView(tv);
    388                 mViewCandidateTemplate = tv;
    389             }
    390             mViewCandidateList1st.addView(lineView);
    391         }
    392     }
    393 
    394     /** @see CandidatesViewManager#getCurrentView */
    395     public View getCurrentView() {
    396         return mViewBody;
    397     }
    398 
    399     /** @see CandidatesViewManager#setViewType */
    400     public void setViewType(int type) {
    401         boolean readMore = setViewLayout(type);
    402 
    403         if (readMore) {
    404             displayCandidates(this.mConverter, false, -1);
    405         } else {
    406             if (type == CandidatesViewManager.VIEW_TYPE_NORMAL) {
    407                 mIsFullView = false;
    408                 if (mDisplayEndOffset > 0) {
    409                     int maxLine = getMaxLine();
    410                     displayCandidates(this.mConverter, false, maxLine);
    411                 } else {
    412                     setReadMore();
    413                 }
    414             } else {
    415                 if (mViewBody.isShown()) {
    416                     mWnn.setCandidatesViewShown(false);
    417                 }
    418             }
    419         }
    420     }
    421 
    422     /**
    423      * Set the view layout
    424      *
    425      * @param type      View type
    426      * @return          {@code true} if display is updated; {@code false} if otherwise
    427      */
    428     private boolean setViewLayout(int type) {
    429         mViewType = type;
    430         setViewScaleUp(false, null);
    431 
    432         switch (type) {
    433         case CandidatesViewManager.VIEW_TYPE_CLOSE:
    434             mViewCandidateBase.setMinimumHeight(-1);
    435             return false;
    436 
    437         case CandidatesViewManager.VIEW_TYPE_NORMAL:
    438             mViewBodyScroll.scrollTo(0, 0);
    439             mViewCandidateList1st.setVisibility(View.VISIBLE);
    440             mViewCandidateList2nd.setVisibility(View.GONE);
    441             mViewCandidateBase.setMinimumHeight(-1);
    442             int line = (mPortrait) ? LINE_NUM_PORTRAIT : LINE_NUM_LANDSCAPE;
    443             mViewCandidateList1st.setMinimumHeight(getCandidateMinimumHeight() * line);
    444             return false;
    445 
    446         case CandidatesViewManager.VIEW_TYPE_FULL:
    447         default:
    448             mViewCandidateList2nd.setVisibility(View.VISIBLE);
    449             mViewCandidateBase.setMinimumHeight(mViewHeight);
    450             return true;
    451         }
    452     }
    453 
    454     /** @see CandidatesViewManager#getViewType */
    455     public int getViewType() {
    456         return mViewType;
    457     }
    458 
    459     /** @see CandidatesViewManager#displayCandidates */
    460     public void displayCandidates(WnnEngine converter) {
    461 
    462         mCanReadMore = false;
    463         mDisplayEndOffset = 0;
    464         mIsFullView = false;
    465         mFullViewWordCount = 0;
    466         mFullViewOccupyCount = 0;
    467         mFullViewPrevLineTopId = 0;
    468         mCreateCandidateDone = false;
    469         mNormalViewWordCountOfLine = 0;
    470 
    471         clearCandidates();
    472         mConverter = converter;
    473         setViewLayout(CandidatesViewManager.VIEW_TYPE_NORMAL);
    474 
    475         mViewCandidateTemplate.setVisibility(View.VISIBLE);
    476         mViewCandidateTemplate.setBackgroundResource(R.drawable.cand_back);
    477 
    478         displayCandidates(converter, true, getMaxLine());
    479     }
    480 
    481     /** @see CandidatesViewManager#getMaxLine */
    482     private int getMaxLine() {
    483         int maxLine = (mPortrait) ? LINE_NUM_PORTRAIT : LINE_NUM_LANDSCAPE;
    484         return maxLine;
    485     }
    486 
    487     /**
    488      * Display the candidates.
    489      *
    490      * @param converter  {@link WnnEngine} which holds candidates.
    491      * @param dispFirst  Whether it is the first time displaying the candidates
    492      * @param maxLine    The maximum number of displaying lines
    493      */
    494     synchronized private void displayCandidates(WnnEngine converter, boolean dispFirst, int maxLine) {
    495         if (converter == null) {
    496             return;
    497         }
    498 
    499         /* Concatenate the candidates already got and the last one in dispFirst mode */
    500         int displayLimit = mDisplayLimit;
    501 
    502         boolean isHistorySequence = false;
    503         boolean isBreak = false;
    504 
    505         /* Get candidates */
    506         WnnWord result = null;
    507         while ((displayLimit == -1 || mWordCount < displayLimit)) {
    508             result = converter.getNextCandidate();
    509 
    510             if (result == null) {
    511                 break;
    512             }
    513 
    514             setCandidate(false, result);
    515 
    516             if (dispFirst && (maxLine < mLineCount)) {
    517                 mCanReadMore = true;
    518                 isBreak = true;
    519                 break;
    520             }
    521         }
    522 
    523         if (!isBreak && !mCreateCandidateDone) {
    524             /* align left if necessary */
    525             createNextLine();
    526             mCreateCandidateDone = true;
    527         }
    528 
    529         if (mWordCount < 1) { /* no candidates */
    530             if (mAutoHideMode) {
    531                 mWnn.setCandidatesViewShown(false);
    532                 return;
    533             } else {
    534                 mCanReadMore = false;
    535                 mIsFullView = false;
    536                 setViewLayout(CandidatesViewManager.VIEW_TYPE_NORMAL);
    537             }
    538         }
    539 
    540         setReadMore();
    541 
    542         if (!(mViewBody.isShown())) {
    543             mWnn.setCandidatesViewShown(true);
    544         }
    545         return;
    546     }
    547 
    548     /**
    549      * Add a candidate into the list.
    550      * @param isCategory  {@code true}:caption of category, {@code false}:normal word
    551      * @param word        A candidate word
    552      */
    553     private void setCandidate(boolean isCategory, WnnWord word) {
    554         int textLength = measureText(word.candidate, 0, word.candidate.length());
    555         TextView template = mViewCandidateTemplate;
    556         textLength += template.getPaddingLeft() + template.getPaddingRight();
    557         int maxWidth = mViewWidth;
    558 
    559         TextView textView;
    560         if (mIsFullView || getMaxLine() < mLineCount) {
    561             /* Full view */
    562             int indentWidth = mViewWidth / FULL_VIEW_DIV;
    563             int occupyCount = Math.min((textLength + indentWidth) / indentWidth, FULL_VIEW_DIV);
    564             if (isCategory) {
    565                 occupyCount = FULL_VIEW_DIV;
    566             }
    567 
    568             if (FULL_VIEW_DIV < (mFullViewOccupyCount + occupyCount)) {
    569                 if (FULL_VIEW_DIV != mFullViewOccupyCount) {
    570                     mFullViewPrevParams.width += (FULL_VIEW_DIV - mFullViewOccupyCount) * indentWidth;
    571                     mViewCandidateList2nd.updateViewLayout(mFullViewPrevView, mFullViewPrevParams);
    572                 }
    573                 mFullViewOccupyCount = 0;
    574                 mFullViewPrevLineTopId = mFullViewPrevView.getId();
    575                 mLineCount++;
    576             }
    577 
    578             ViewGroup layout = mViewCandidateList2nd;
    579 
    580             int width = indentWidth * occupyCount;
    581             int height = getCandidateMinimumHeight();
    582 
    583 
    584             ViewGroup.LayoutParams params = buildLayoutParams(mViewCandidateList2nd, width, height);
    585 
    586             textView = (TextView) layout.getChildAt(mFullViewWordCount);
    587             if (textView == null) {
    588                 textView = createCandidateView();
    589                 textView.setLayoutParams(params);
    590 
    591                 mViewCandidateList2nd.addView(textView);
    592             } else {
    593                 mViewCandidateList2nd.updateViewLayout(textView, params);
    594             }
    595 
    596             mFullViewOccupyCount += occupyCount;
    597             mFullViewWordCount++;
    598             mFullViewPrevView = textView;
    599             mFullViewPrevParams = params;
    600 
    601         } else {
    602             textLength = Math.max(textLength, getCandidateMinimumWidth());
    603 
    604             /* Normal view */
    605             int nextEnd = mLineLength + textLength;
    606             if (mLineCount == 1) {
    607                 maxWidth -= getCandidateMinimumWidth();
    608             }
    609 
    610             if ((maxWidth < nextEnd) && (mWordCount != 0)) {
    611                 createNextLine();
    612                 if (getMaxLine() < mLineCount) {
    613                     mLineLength = 0;
    614                     /* Call this method again to add the candidate in the full view */
    615                     setCandidate(isCategory, word);
    616                     return;
    617                 }
    618 
    619                 mLineLength = textLength;
    620             } else {
    621                 mLineLength = nextEnd;
    622             }
    623 
    624             LinearLayout lineView = (LinearLayout) mViewCandidateList1st.getChildAt(mLineCount - 1);
    625             textView = (TextView) lineView.getChildAt(mNormalViewWordCountOfLine);
    626 
    627             if (isCategory) {
    628                 if (mLineCount == 1) {
    629                     mViewCandidateTemplate.setBackgroundDrawable(null);
    630                 }
    631                 mLineLength += CANDIDATE_LEFT_ALIGN_THRESHOLD;
    632             }
    633 
    634             mNormalViewWordCountOfLine++;
    635         }
    636 
    637         textView.setText(word.candidate);
    638         textView.setTextColor(mTextColor);
    639         textView.setId(mWordCount);
    640         textView.setVisibility(View.VISIBLE);
    641         textView.setPressed(false);
    642 
    643         if (isCategory) {
    644             textView.setOnClickListener(null);
    645             textView.setOnLongClickListener(null);
    646             textView.setBackgroundDrawable(null);
    647         } else {
    648             textView.setOnClickListener(mCandidateOnClick);
    649             textView.setOnLongClickListener(mCandidateOnLongClick);
    650             textView.setBackgroundResource(R.drawable.cand_back);
    651         }
    652         textView.setOnTouchListener(mCandidateOnTouch);
    653 
    654         if (maxWidth < textLength) {
    655             textView.setEllipsize(TextUtils.TruncateAt.END);
    656         } else {
    657             textView.setEllipsize(null);
    658         }
    659 
    660         ImageSpan span = null;
    661         if (word.candidate.equals(" ")) {
    662             span = new ImageSpan(mWnn, R.drawable.word_half_space,
    663                                  DynamicDrawableSpan.ALIGN_BASELINE);
    664         } else if (word.candidate.equals("\u3000" /* full-width space */)) {
    665             span = new ImageSpan(mWnn, R.drawable.word_full_space,
    666                                  DynamicDrawableSpan.ALIGN_BASELINE);
    667         }
    668 
    669         if (span != null) {
    670             SpannableString spannable = new SpannableString("   ");
    671             spannable.setSpan(span, 1, 2, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    672             textView.setText(spannable);
    673         }
    674 
    675         mWnnWordArray.add(mWordCount, word);
    676         mWordCount++;
    677     }
    678 
    679     /**
    680      * Create AbsoluteLayout.LayoutParams
    681      * @param layout AbsoluteLayout
    682      * @param width
    683      * @param height
    684      * @return ViewGroup.LayoutParams
    685      */
    686     private ViewGroup.LayoutParams buildLayoutParams(AbsoluteLayout layout, int width, int height) {
    687 
    688         int indentWidth = mViewWidth / FULL_VIEW_DIV;
    689         int x         = indentWidth * mFullViewOccupyCount;
    690         int nomalLine = (mPortrait) ? LINE_NUM_PORTRAIT : LINE_NUM_LANDSCAPE;
    691         int y         = getCandidateMinimumHeight() * (mLineCount - nomalLine - 1);
    692         ViewGroup.LayoutParams params
    693               = new AbsoluteLayout.LayoutParams(width, height, x, y);
    694 
    695         return params;
    696     }
    697 
    698 
    699 
    700 
    701 
    702 
    703     /**
    704      * Create a view for a candidate.
    705      * @return the view
    706      */
    707     private TextView createCandidateView() {
    708         TextView text = new TextView(mViewBodyScroll.getContext());
    709         text.setTextSize(20);
    710         text.setBackgroundResource(R.drawable.cand_back);
    711         text.setGravity(Gravity.CENTER);
    712         text.setSingleLine();
    713         text.setPadding(4, 4, 4, 4);
    714         text.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
    715                                                            ViewGroup.LayoutParams.WRAP_CONTENT,
    716                                                            1.0f));
    717         text.setMinHeight(getCandidateMinimumHeight());
    718         text.setMinimumWidth(getCandidateMinimumWidth());
    719         return text;
    720     }
    721 
    722     /**
    723      * Display {@code mReadMoreText} if there are more candidates.
    724      */
    725     private void setReadMore() {
    726         if (mIsScaleUp) {
    727             mReadMoreButton.setVisibility(View.GONE);
    728             mViewCandidateTemplate.setVisibility(View.GONE);
    729             return;
    730         }
    731 
    732         if (mIsFullView) {
    733             mReadMoreButton.setVisibility(View.VISIBLE);
    734             mReadMoreButton.setImageResource(R.drawable.cand_down);
    735         } else {
    736             if (mCanReadMore) {
    737                 mReadMoreButton.setVisibility(View.VISIBLE);
    738                 mReadMoreButton.setImageResource(R.drawable.cand_up);
    739             } else {
    740                 mReadMoreButton.setVisibility(View.GONE);
    741                 mViewCandidateTemplate.setVisibility(View.GONE);
    742             }
    743         }
    744     }
    745 
    746     /**
    747      * Clear the list of the normal candidate view.
    748      */
    749     private void clearNormalViewCandidate() {
    750         LinearLayout candidateList = mViewCandidateList1st;
    751         int lineNum = candidateList.getChildCount();
    752         for (int i = 0; i < lineNum; i++) {
    753 
    754             LinearLayout lineView = (LinearLayout)candidateList.getChildAt(i);
    755             int size = lineView.getChildCount();
    756             for (int j = 0; j < size; j++) {
    757                 View v = lineView.getChildAt(j);
    758                 v.setVisibility(View.GONE);
    759             }
    760         }
    761     }
    762 
    763     /** @see CandidatesViewManager#clearCandidates */
    764     public void clearCandidates() {
    765         clearNormalViewCandidate();
    766 
    767         ViewGroup layout = mViewCandidateList2nd;
    768         int size = layout.getChildCount();
    769         for (int i = 0; i < size; i++) {
    770             View v = layout.getChildAt(i);
    771             v.setVisibility(View.GONE);
    772         }
    773 
    774         mLineCount = 1;
    775         mWordCount = 0;
    776         mWnnWordArray.clear();
    777 
    778         mLineLength = 0;
    779 
    780         mIsFullView = false;
    781         setViewLayout(CandidatesViewManager.VIEW_TYPE_NORMAL);
    782         if (mAutoHideMode) {
    783             setViewLayout(CandidatesViewManager.VIEW_TYPE_CLOSE);
    784         }
    785 
    786         if (mAutoHideMode && mViewBody.isShown()) {
    787             mWnn.setCandidatesViewShown(false);
    788         }
    789         mCanReadMore = false;
    790         setReadMore();
    791     }
    792 
    793     /** @see CandidatesViewManager#setPreferences */
    794     public void setPreferences(SharedPreferences pref) {
    795         try {
    796             if (pref.getBoolean("key_vibration", false)) {
    797                 mVibrator = (Vibrator)mWnn.getSystemService(Context.VIBRATOR_SERVICE);
    798             } else {
    799                 mVibrator = null;
    800             }
    801             if (pref.getBoolean("key_sound", false)) {
    802                 mSound = MediaPlayer.create(mWnn, R.raw.type);
    803             } else {
    804                 mSound = null;
    805             }
    806         } catch (Exception ex) {
    807             Log.d("iwnn", "NO VIBRATOR");
    808         }
    809     }
    810 
    811     /**
    812      * Process {@code OpenWnnEvent.CANDIDATE_VIEW_TOUCH} event.
    813      *
    814      * @return      {@code true} if event is processed; {@code false} if otherwise
    815      */
    816     public boolean onTouchSync() {
    817         return mGestureDetector.onTouchEvent(mMotionEvent);
    818     }
    819 
    820     /**
    821      * Select a candidate.
    822      * <br>
    823      * This method notices the selected word to {@link OpenWnn}.
    824      *
    825      * @param word  The selected word
    826      */
    827     private void selectCandidate(WnnWord word) {
    828         setViewLayout(CandidatesViewManager.VIEW_TYPE_NORMAL);
    829         if (mVibrator != null) {
    830             try { mVibrator.vibrate(30); } catch (Exception ex) { }
    831         }
    832         if (mSound != null) {
    833             try { mSound.seekTo(0); mSound.start(); } catch (Exception ex) { }
    834         }
    835         mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.SELECT_CANDIDATE, word));
    836     }
    837 
    838     /** @see android.view.GestureDetector.OnGestureListener#onDown */
    839     public boolean onDown(MotionEvent arg0) {
    840         return false;
    841     }
    842 
    843     /** @see android.view.GestureDetector.OnGestureListener#onFling */
    844     public boolean onFling(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
    845         if (mIsScaleUp) {
    846             return false;
    847         }
    848 
    849         boolean consumed = false;
    850         if (arg1 != null && arg0 != null && arg1.getY() < arg0.getY()) {
    851             if ((mViewType == CandidatesViewManager.VIEW_TYPE_NORMAL) && mCanReadMore) {
    852                 if (mVibrator != null) {
    853                     try { mVibrator.vibrate(30); } catch (Exception ex) { }
    854                 }
    855                 mIsFullView = true;
    856                 mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.LIST_CANDIDATES_FULL));
    857                 consumed = true;
    858             }
    859         } else {
    860             if (mViewBodyScroll.getScrollY() == 0) {
    861                 if (mVibrator != null) {
    862                     try { mVibrator.vibrate(30); } catch (Exception ex) { }
    863                 }
    864                 mIsFullView = false;
    865                 mWnn.onEvent(new OpenWnnEvent(OpenWnnEvent.LIST_CANDIDATES_NORMAL));
    866                 consumed = true;
    867             }
    868         }
    869 
    870         return consumed;
    871     }
    872 
    873     /** @see android.view.GestureDetector.OnGestureListener#onLongPress */
    874     public void onLongPress(MotionEvent arg0) {
    875         return;
    876     }
    877 
    878     /** @see android.view.GestureDetector.OnGestureListener#onScroll */
    879     public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
    880         return false;
    881     }
    882 
    883     /** @see android.view.GestureDetector.OnGestureListener#onShowPress */
    884     public void onShowPress(MotionEvent arg0) {
    885     }
    886 
    887     /** @see android.view.GestureDetector.OnGestureListener#onSingleTapUp */
    888     public boolean onSingleTapUp(MotionEvent arg0) {
    889         return false;
    890     }
    891 
    892     /**
    893      * Retrieve the width of string to draw.
    894      *
    895      * @param text          The string
    896      * @param start         The start position (specified by the number of character)
    897      * @param end           The end position (specified by the number of character)
    898      * @return          The width of string to draw
    899      */
    900     public int measureText(CharSequence text, int start, int end) {
    901         if (end - start < 3) {
    902             return getCandidateMinimumWidth();
    903         }
    904 
    905         TextPaint paint = mViewCandidateTemplate.getPaint();
    906         return (int)paint.measureText(text, start, end);
    907     }
    908 
    909     /**
    910      * Switch list/enlarge view mode.
    911      * @param up  {@code true}:enlarge, {@code false}:list
    912      * @param word  The candidate word to be enlarged.
    913      */
    914     private void setViewScaleUp(boolean up, WnnWord word) {
    915         if (up == mIsScaleUp || (mViewScaleUp == null)) {
    916             return;
    917         }
    918 
    919         if (up) {
    920             setViewLayout(CandidatesViewManager.VIEW_TYPE_NORMAL);
    921             mViewCandidateList1st.setVisibility(View.GONE);
    922             mViewCandidateBase.setMinimumHeight(-1);
    923             mViewCandidateBase.addView(mViewScaleUp);
    924             TextView text = (TextView)mViewScaleUp.findViewById(R.id.candidate_scale_up_text);
    925             text.setText(word.candidate);
    926             if (!mPortrait) {
    927                 Resources r = mViewBodyScroll.getContext().getResources();
    928                 text.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_delete_word_size_landscape));
    929             }
    930 
    931             mIsScaleUp = true;
    932             setReadMore();
    933         } else {
    934             mIsScaleUp = false;
    935             mViewCandidateBase.removeView(mViewScaleUp);
    936         }
    937     }
    938 
    939     /**
    940      * Create a layout for the next line.
    941      */
    942     private void createNextLine() {
    943         int lineCount = mLineCount;
    944         if (mIsFullView || getMaxLine() < lineCount) {
    945             /* Full view */
    946             mFullViewOccupyCount = 0;
    947             mFullViewPrevLineTopId = mFullViewPrevView.getId();
    948         } else {
    949             /* Normal view */
    950             LinearLayout lineView = (LinearLayout) mViewCandidateList1st.getChildAt(lineCount - 1);
    951             float weight = 0;
    952             if (mLineLength < CANDIDATE_LEFT_ALIGN_THRESHOLD) {
    953                 if (lineCount == 1) {
    954                     mViewCandidateTemplate.setVisibility(View.GONE);
    955                 }
    956             } else {
    957                 weight = 1.0f;
    958             }
    959 
    960             LinearLayout.LayoutParams params
    961                 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
    962                                                 ViewGroup.LayoutParams.WRAP_CONTENT,
    963                                                 weight);
    964 
    965             int child = lineView.getChildCount();
    966             for (int i = 0; i < child; i++) {
    967                 View view = lineView.getChildAt(i);
    968 
    969                 if (view != mViewCandidateTemplate) {
    970                     view.setLayoutParams(params);
    971                 }
    972             }
    973 
    974             mLineLength = 0;
    975             mNormalViewWordCountOfLine = 0;
    976         }
    977         mLineCount++;
    978     }
    979 
    980     /**
    981      * @return the minimum width of a candidate view.
    982      */
    983     private int getCandidateMinimumWidth() {
    984         return mCandidateMinimumWidth;
    985     }
    986 
    987     /**
    988      * @return the minimum height of a candidate view.
    989      */
    990     private int getCandidateMinimumHeight() {
    991         return mCandidateMinimumHeight;
    992     }
    993 }
    994