本文是介绍openwnn源码的第三篇,将要介绍的内容是日文输入法的CandidatesView。
1、相关功能
为了介绍源码,当然需要介绍一下这个CandidatesView的样式及功能。(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);
}
首先它定义了候选词列表的三种状态:普通、全屏、关闭。这个大家看第第1部分那两张图就明白了。
另外它还定义后了候选词的属性,这些词有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;
}
这里我们看到CandidatesView显示的view主要有三个:mViewBodyText,mReadMoreText,mViewBodyScroll 。mViewBodyText是个EditText,我们看到的候选词列表由它显示;mReadMoreText是一个TextView,就是一个“more”按钮,点击可以查看更多的候选词。另外mSelectBottonText,mCancelBottonText 这两个是按钮来着,用于长按一个候选词时弹出的对话框。
这里主要是设置一些参数,大家具体看代码了。
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();
}
}
}
}
这一步主要是设置CandidatesView的状态,同时根据其状态显示候选词(其中调用了显示候选词函数displayCandidates)。设置状态的主要函数在setViewLayout,这个主要是设置显示的行数等等信息。而其中addNewlineIfNecessary函数的作用则是,修改了CandidatesView的状态以后,候选词的数量可能比较少,这时候需要将空余部分显示为空行。
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) {
这里前者是对外接口,后者则是私有函数。注意这里有一个synchronized关键字,我猜测是用于保护显示,也就是前一次CandidatesView显示完成以后,才可以进行后一次显示,这样就不会错乱。这在快速输入的时候是比较有用的。
对于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;
}
这里实际上就是获得每一个候选词,判断如果将该候选词放入当前行,看当前行是否大于(mViewWidth - padding)。如果大于,则说明当前行已经放不下该候选词了,需要另起一行;若是不大于,则说明当前可以放得下该候选词,于是继续选取下一个候选词看是否可以放入当前行。
另外,这里讲一下导入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);
}
这段代码,我猜测是将每一个偏移位置所对应的候选词编号都记录下来。因此你获得一个便宜位置就可以通过查询mPositionToWordIndexArray这个数组来找到其所对应的候选词编号。
于是,用户如果按住某个候选词时,会调用如下函数:
/** 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)问题,其实没有太大所谓。