写一个Android输入法02——候选窗、转换
上一篇介绍了完成Android输入法的最小化步骤,它只能将按键对应的字符上屏。一般的东亚语言都有一个转换的过程,比如汉语输入拼音,需要由拼音转成汉字再上屏。本文将在前文基础上加入完成转换过程所必需的候选窗。本文代码可参见https://github.com/palanceli/AndroidXXIME/tree/v2。
如下图所示,用红框框出来的窗体是候选窗,其内的字符创叫做候选串,点击候选窗使之进入输入控件叫做上屏。没有输入的时候隐藏候选窗,当输入字串还未上屏时显示候选窗:
引入候选窗需要完成两个步骤:
一、创建CandidateView,该窗口需要覆盖如下两个方法,已完成自绘:
- onDraw(Canvas canvas);
- onMeasure(int widthMeasureSpec, int heightMeasureSpec);
二、覆盖AndroidXXIME类的如下两个方法:
- onCreateCandidateView();
在该方法中创建CandidateView。
- onKey(int primaryCode, int [] keyCodes);
在该方法中响应按键消息,如:当按下字母键,则展现候选窗以及候选字串;当按下空格,则上屏候选字串,等等。
创建CandidateView
public CandidateView(Context context) { super(context); Log.d(this.getClass().toString(), "CandidateView: "); // 设置前景、背景色、字体、字号 Resources r = context.getResources(); setBackgroundColor(getResources().getColor(R.color.candidate_background, null)); mColorNormal = r.getColor(R.color.candidate_normal, null); mVerticalPadding = r.getDimensionPixelSize(R.dimen.candidate_vertical_padding); mPaint = new Paint(); mPaint.setColor(mColorNormal); mPaint.setAntiAlias(true); mPaint.setTextSize(r.getDimensionPixelSize(R.dimen.candidate_font_height)); mPaint.setStrokeWidth(0); setWillNotDraw(false); // 覆盖了onDraw函数应清除该标记 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.d(this.getClass().toString(), "onMeasure: "); int wMode = MeasureSpec.getMode(widthMeasureSpec); int wSize = MeasureSpec.getSize(widthMeasureSpec); int measuredWidth = resolveSize(50, widthMeasureSpec); final int desiredHeight = ((int)mPaint.getTextSize()) + mVerticalPadding; // 系统会根据返回值确定窗体的大小 setMeasuredDimension(measuredWidth, resolveSize(desiredHeight, heightMeasureSpec)); } @Override protected void onDraw(Canvas canvas) { Log.d(this.getClass().toString(), "onDraw: "); super.onDraw(canvas); if (mSuggestions == null) return; // 依次绘制每组候选字串 int x = 0; final int count = mSuggestions.size(); final int height = getHeight(); final int y = (int) (((height - mPaint.getTextSize()) / 2) - mPaint.ascent()); for (int i = 0; i < count; i++) { String suggestion = mSuggestions.get(i); float textWidth = mPaint.measureText(suggestion); final int wordWidth = (int) textWidth + X_GAP * 2; canvas.drawText(suggestion, x + X_GAP, y, mPaint); x += wordWidth; } } public void setSuggestions(List<String> suggestions) { // 设置候选字串列表 if (suggestions != null) { mSuggestions = new ArrayList<String>(suggestions); } invalidate(); requestLayout(); } }
覆盖onCreateCandidateView()方法
该方法会在每次输入法被呼出的时候调用,如函数名所示,在这里创建候选窗口。
public class AndroidXXIME extends InputMethodService implements KeyboardView.OnKeyboardActionListener { …… @Override public View onCreateCandidatesView(){ Log.d(this.getClass().toString(), "onCreateCandidatesView: "); candidateView = new CandidateView(this); return candidateView; } …… }
覆盖onKey(int primaryCode, int [] keyCodes)方法
public class AndroidXXIME extends InputMethodService implements KeyboardView.OnKeyboardActionListener { …… @Override public void onKey(int primaryCode, int[] keyCodes) { InputConnection ic = getCurrentInputConnection(); playClick(primaryCode); switch(primaryCode){ case Keyboard.KEYCODE_DELETE : // 如果收到的是DELETE键,则删除光标前的一个字符 ic.deleteSurroundingText(1, 0); break; case Keyboard.KEYCODE_DONE: // 如果收到的是DONE键,则执行回车 ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); break; default: char code = (char)primaryCode; if(code == ' '){ // 如果收到的是空格 if(m_composeString.length() > 0) { // 如果有写作串,则将首个候选提交上屏 ic.commitText(m_composeString, m_composeString.length()); m_composeString.setLength(0); }else{ // 如果没有写作串,则直接将空格上屏 ic.commitText(" ", 1); } }else { // 否则,将字符计入写作串 m_composeString.append(code); ic.setComposingText(m_composeString, 1); } updateCandidates(); } } }
在updateCandidates()函数中向CandidateView塞入候选字串列表,并触发该窗口更新。
当在系统“语言和输入法”-“更改键盘”中选择输入法时,
系统会调用该输入法InputMethodService的如下方法:
- onCreate()
- onInitializeInterface() 可以在该方法中完成与输入法相关的初始化操作,比如加载词库。
- onStartInput() 每次切换输入焦点的时候,都会调用该方法,在这里可以完成和会话相关的初始化操作,后面还会介绍。
当一个输入控件获得焦点,呼出输入法,到它失去焦点,这期间成为一次会话。当一个会话开始时,系统会调用输入法InputMethodService的如下方法:
- onStartInput() 负责会话相关的初始化工作。输入法要负责在会话切换时,清除上次会话的中间数据,以防止前一个会话的中间数据窜入下个会话。这和Windows平台下的输入法有很大区别,在Windows下,输入法一个DLL,它依附在切出输入法的进程中,因此,每个输入法进程保存各自的输入法上下文,如果需要在进程间共享数据(比如词库),则需要采用共享内存机制;而在Android平台下,输入法是一个独立的进程,所有的数据仅在该进程中保存一份,此时则需要考虑如何隔离不同进程间的私有数据,比如前一个进程输入一半但未上屏的数据,切到另一个进程或输入控件后,就应该清除掉。该回调函数用来做这类工作。
- onCreateInputView() 创建输入法键盘布局。
- onCreateCandidatesView() 创建候选窗。
完成以上步骤之后,输入法就多出了候选窗口,下图中浅蓝色窗体既是:
在处理上还是很简陋,比如退格还不支持删除输入串,还不支持点击上屏,等等。这些属于业务逻辑的细节了,可以慢慢精耕细作。