本文是介绍openwnn源码的第三篇,将要介绍的内容是日文输入法的CandidatesView。
1、相关功能
为了介绍源码,当然需要介绍一下这个CandidatesView的样式及功能。由于我没有去编译openwnn源码,因此只能以android模拟器自带的openwnn日文输入法(japanese ime)来介绍。具体功能根据我对该输入法的使用和对openwnn源码的阅读,应该是没有多大差别的。(android2.2的模拟器)
首先来看一下功能截图:
第一张是输入あ的候选框图,第二张是点击第一张那个向上箭头后的候选框图。也就是说,第一张只是显示了部分候选词,第二张则是显示了所有的候选词,同时如果满屏都无法显示所有的候选词,则还有一个滚动条。
如果你单击某一个候选词,则该候选词上屏。若你长按一个候选词,则候选词会变为如下格式:
点击关闭,则恢复到长按以前的状态,若点选择,则该候选词上屏。
2 CandidatesViewManager
在源码中,涉及到CandidatesView的只有两个类:CandidatesViewManager.java和TextCandidatesViewManager.java。前者是通用接口,后者是具体的是实现类。这里你会发现不管哪种语言,使用的都是这两个类,并没有对TextCandidatesViewManager类进行继承。说明这个设计还是比较好(或者会不会CandidatesView本身就比较简单)。
首先我们来看一下CandidatesViewManager.java,这是一个接口类。输入法只要使用这个接口就可以了,并不需要关注其实现细节。其代码比较简答,如下所示:
- /**
- * The interface of candidates view manager used by {@link OpenWnn}.
- *
- * @author Copyright (C) 2008, 2009 OMRON SOFTWARE CO., LTD. All Rights Reserved.
- */
- public interface CandidatesViewManager {
- /** Size of candidates view (normal) */
- public static final int VIEW_TYPE_NORMAL = 0;
- /** Size of candidates view (full) */
- public static final int VIEW_TYPE_FULL = 1;
- /** Size of candidates view (close/non-display) */
- public static final int VIEW_TYPE_CLOSE = 2;
- /**
- * Attribute of a word (no attribute)
- * @see jp.co.omronsoft.openwnn.WnnWord
- */
- public static final int ATTRIBUTE_NONE = 0;
- /**
- * Attribute of a word (a candidate in the history list)
- * @see jp.co.omronsoft.openwnn.WnnWord
- */
- public static final int ATTRIBUTE_HISTORY = 1;
- /**
- * Attribute of a word (the best candidate)
- * @see jp.co.omronsoft.openwnn.WnnWord
- */
- public static final int ATTRIBUTE_BEST = 2;
- /**
- * Attribute of a word (auto generated/not in the dictionary)
- * @see jp.co.omronsoft.openwnn.WnnWord
- */
- public static final int ATTRIBUTE_AUTO_GENERATED = 4;
- /**
- * Initialize the candidates view.
- *
- * @param parent The OpenWnn object
- * @param width The width of the display
- * @param height The height of the display
- *
- * @return The candidates view created in the initialize process; {@code null} if cannot create a candidates view.
- */
- public View initView(OpenWnn parent, int width, int height);
- /**
- * Get the candidates view being used currently.
- *
- * @return The candidates view; {@code null} if no candidates view is used currently.
- */
- public View getCurrentView();
- /**
- * Set the candidates view type.
- *
- * @param type The candidate view type
- */
- public void setViewType(int type);
- /**
- * Get the candidates view type.
- *
- * @return The view type
- */
- public int getViewType();
- /**
- * Display candidates.
- *
- * @param converter The {@link WnnEngine} from which {@link CandidatesViewManager} gets the candidates
- *
- * @see jp.co.omronsoft.openwnn.WnnEngine#getNextCandidate
- */
- public void displayCandidates(WnnEngine converter);
- /**
- * Clear and hide the candidates view.
- */
- public void clearCandidates();
- /**
- * Reflect the preferences in the candidates view.
- *
- * @param pref The preferences
- */
- public void setPreferences(SharedPreferences pref);
- }
另外它还定义后了候选词的属性,这些词有4种属性,具体看英文注释。
大家比较关注的应该是它提供给输入法使用的接口,可以看出需要提供给外界使用的接口是比较少的。
3 TextCandidatesViewManager
这一部分是CandidatesView的具体实现类。我们直观上看CandidatesView主要的功能应该是获得并显示候选词,并做一些功能方便用户使用。因此我们按照CandidatesViewManager类中的接口来介绍一下其实现方式。
3.1 initView
这一部分初始化CandidatesView。其代码如下:
- /** @see CandidatesViewManager */
- public View initView(OpenWnn parent, int width, int height) {
- mWnn = parent;
- mViewWidth = width;
- mSelectBottonText = mWnn.getResources().getString(R.string.button_candidate_select);
- mCancelBottonText = mWnn.getResources().getString(R.string.button_candidate_cancel);
- mViewBody = (ViewGroup)parent.getLayoutInflater().inflate(R.layout.candidates, null);
- mViewBodyScroll = (ScrollView)mViewBody.findViewById(R.id.candview_scroll);
- mViewBodyScroll.setOnTouchListener(this);
- mViewBodyText = (EditText)mViewBody.findViewById(R.id.text_candidates_view);
- mViewBodyText.setOnTouchListener(this);
- mViewBodyText.setTextSize(18.0f);
- mViewBodyText.setLineSpacing(6.0f, 1.5f);
- mViewBodyText.setIncludeFontPadding(false);
- mViewBodyText.setFocusable(true);
- mViewBodyText.setCursorVisible(false);
- mViewBodyText.setGravity(Gravity.TOP);
- mReadMoreText = (TextView)mViewBody.findViewById(R.id.read_more_text);
- mReadMoreText.setText(mWnn.getResources().getString(R.string.read_more));
- mReadMoreText.setTextSize(24.0f);
- mPortrait = (height > 450)? true : false;
- setViewType(CandidatesViewManager.VIEW_TYPE_CLOSE);
- mGestureDetector = new GestureDetector(this);
- return mViewBody;
- }
这里主要是设置一些参数,大家具体看代码了。
3.2 setViewType
这一步主要是设置CandidatesView的状态,候选词列表的三种状态:普通、全屏、关闭。在上面初始化view的时候是将其设置为关闭状态。这一部分的代码:
- /** @see CandidatesViewManager#setViewType */
- public void setViewType(int type) {
- boolean readMore = setViewLayout(type);
- addNewlineIfNecessary();
- if (readMore) {
- displayCandidates(this.mConverter, false, -1);
- } else {
- if (type == CandidatesViewManager.VIEW_TYPE_NORMAL) {
- mIsFullView = false;
- if (mDisplayEndOffset > 0) {
- int maxLine = getMaxLine();
- displayCandidates(this.mConverter, false, maxLine);
- } else {
- setReadMore();
- }
- }
- }
- }
3.3 displayCandidates
这个函数是这里面最重要的函数,其功能是根据候选词引擎获得候选词,同时显示候选词。这个函数是经过重载的,它由两个形式:
- /** @see CandidatesViewManager#displayCandidates */
- public void displayCandidates(WnnEngine converter) {
- mCanReadMore = false;
- mDisplayEndOffset = 0;
- mIsFullView = false;
- int maxLine = getMaxLine();
- displayCandidates(converter, true, maxLine);
- }
- /**
- * Display the candidates.
- *
- * @param converter {@link WnnEngine} which holds candidates.
- * @param dispFirst Whether it is the first time displaying the candidates
- * @param maxLine The maximum number of displaying lines
- */
- synchronized private void displayCandidates(WnnEngine converter, boolean dispFirst, int maxLine) {
对于displayCandidates函数的具体实现,由于我没有很详细的去看,所以无法讲。但是根据大概的了解,我猜测是这样的:首先初始化;获得下一个候选词同时生成该候选词的显示信息(比如一个很长的候选词可能需要分在多行显示)。
3.3.1 createDisplayText
这里获得下一个候选词并生成该候选词的显示信息主要由createDisplayText函数完成的。该函数源码如下:
- /**
- * Create the string to show in the candidate window.
- *
- * @param word A candidate word
- * @param maxLine The maximum number of line in the candidate window
- * @return The string to show
- */
- private StringBuffer createDisplayText(WnnWord word, int maxLine) {
- StringBuffer tmp = new StringBuffer();
- int padding = ViewConfiguration.getScrollBarSize() +
- mViewBodyText.getPaddingLeft() +
- mViewBodyText.getPaddingRight();
- int width = mViewWidth - padding;
- TextPaint p = mViewBodyText.getPaint();
- float newLineLength = measureText(p, word.candidate, 0, word.candidate.length());
- float separatorLength = measureText(p, CANDIDATE_SEPARATOR, 0, CANDIDATE_SEPARATOR.length());
- boolean isFirstWordOfLine = (mLineLength == 0);//mLineLength记录的是一行的长度
- int maxWidth = 0;
- int lineLength = 0;
- lineLength += newLineLength;
- maxWidth += width - separatorLength;
- mLineLength += newLineLength;
- mLineLength += separatorLength;
- mLineWordCount++;
- if (mLineWordCount == 0) {
- mLineLength = lineLength;
- mLineLength += separatorLength;
- }
- if (!isFirstWordOfLine && (width < mLineLength) && mLineWordCount != 0) {
- tmp.append("\n"); //此时说明如果将word放入这一行,则这一行就会太长了,因此需要将word放入下一行
- mLineLength = lineLength;
- mLineLength += separatorLength;
- mLineWordCount = 0;
- }
- return adjustDisplaySize(word, tmp, lineLength, maxWidth, maxLine);
- }
这里最重要的就是根据该候选词的长度来生成候选词列表的显示信息,也就是说该候选词是否在下一行显示或者分多行显示等。其中还调用了一个adjustDisplaySize。这个函数从函数名就可以猜出大概了。
3.3.2 countLineUsingMeasureText
这个函数的作用是利用需要输出的字符串,来计算需要多少行来显示。这其中涉及几个部分:1、跟字体大小有关;2、跟候选词间隔有关;3、跟候选框与屏幕左右边的间隔有关;4、跟滚动条的宽度有关;5、当然跟屏幕大小等有关系了。程序大概是利用这些信息来计算CandidatesView展示这些候选词需要多少行来显示。其代码如下:
- /**
- * Count lines using {@link Paint#measureText}.
- *
- * @param text The text to display
- * @return Number of lines
- */
- private int countLineUsingMeasureText(CharSequence text) {
- StringBuffer tmpText = new StringBuffer(text);
- mStartPositionArray.add(mWordCount,tmpText.length());
- int padding =
- ViewConfiguration.getScrollBarSize() +
- mViewBodyText.getPaddingLeft() +
- mViewBodyText.getPaddingRight();
- TextPaint p = mViewBodyText.getPaint();
- int lineCount = 1;
- int start = 0;
- for (int i = 0; i < mWordCount; i++) {
- if (tmpText.length() < start ||
- tmpText.length() < mStartPositionArray.get(i + 1)) {
- return 1;
- }
- float lineLength = measureText(p, tmpText, start, mStartPositionArray.get(i + 1));
- if (lineLength > (mViewWidth - padding)) {
- lineCount++;
- start = mStartPositionArray.get(i);
- i--;
- }
- }
- return lineCount;
- }
另外,这里讲一下导入openwnn源码时,measureText是有错的。因为它导入的是android.text.styled类(至少在2.1以后的android代码中已经找不到android.text.styled了)。正确的方法是,将import android.text.styled这一句删除,同时measureText函数修改为如下形式:
- public int measureText(TextPaint paint, CharSequence text, int start, int end) {
- return (int)paint.measureText(text, start, end);
- }
3.4 用户操作处理
另外这个类有很大一部分篇幅是用来处理用户操作的,比如选择候选词。我们来回顾下该类的申明
- public class TextCandidatesViewManager implements CandidatesViewManager, OnTouchListener,
- GestureDetector.OnGestureListener
对于用户操作,我想最简单的莫过于用户点击某个候选词然后该候选词上屏。这里,我想大家可以想到一个问题,就是用户点击的是屏幕上的某一点,系统怎么知道所选择的哪个候选词并将该候选词上屏呢?这里就会有一个将坐标转化为候选词的操作:
- /**
- * Convert a coordinate into the offset of character
- *
- * @param x The horizontal position
- * @param y The vertical position
- * @return The offset of character
- */
- public int getOffset(int x,int y){
- Layout layout = mViewBodyText.getLayout();
- int line = layout.getLineForVertical(y);
- if( y >= layout.getLineTop(line+1) ){
- return layout.getText().length();
- }
- int offset = layout.getOffsetForHorizontal(line,x);
- offset -= TOUCH_ADJUSTED_VALUE;
- if (offset < 0) {
- offset = 0;
- }
- return offset;
- }
这里主要是通过坐标获得某个候选词的偏移量,而该偏移量是在mPositionToWordIndexArray中定义的。对于mPositionToWordIndexArray中值的确定,我们可以在displayCandidates函数中看到:
- /* save the candidate string */
- mCandidates.delete(0, mCandidates.length());
- mCandidates.append(tmp);
- int j = 0;
- for (int i = 0; i < mWordCount; i++) {
- while (j <= mEndPositionArray.get(i)) {
- if (j < mStartPositionArray.get(i)) {
- mPositionToWordIndexArray.add(j,-1);
- } else {
- mPositionToWordIndexArray.add(j,i);
- }
- j++;
- }
- mPositionToWordIndexArray.add(j,-1);
- mPositionToWordIndexArray.add(j + 1,-1);
- }
于是,用户如果按住某个候选词时,会调用如下函数:
- /** from GestureDetector.OnGestureListener class */
- public boolean onDown(MotionEvent arg0) {
- if (!mCandidateDeleteState) {
- int position = getOffset((int)arg0.getX(),(int)arg0.getY());
- int wordIndex = mPositionToWordIndexArray.get(position);
- if (wordIndex != -1) {
- int startPosition = mStartPositionArray.get(wordIndex);
- int endPosition = 0;
- if (mDisplayEndOffset > 0 && getViewType() == CandidatesViewManager.VIEW_TYPE_NORMAL) {
- endPosition = mDisplayEndOffset + CANDIDATE_SEPARATOR.length();
- } else {
- endPosition = mEndPositionArray.get(wordIndex);
- }
- mViewBodyText.setSelection(startPosition, endPosition);
- mViewBodyText.setCursorVisible(true);
- mViewBodyText.invalidate();
- mHasStartedSelect = true;
- }
- }
- return true;
- }
4、其他
本文主要是对CandidatesView的形成过程做了一个大概的介绍。但是由于时间和能力有限,对如下几个问题未能深入,后续有时间会补上。
1)displayCandidates的具体实现细节
2)我一直有个疑问,显示候选词的既然是一个EditText,那作为一个编辑框,为什么我可以显示一个候选词列表,而且可以点击?
3)用户手势操作的具体分析
第2)问题,估计解决第1)个问题后就可以解决了。第3)问题,其实没有太大所谓。