上一篇介紹了完成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() 創建候選窗。
完成以上步驟之后,輸入法就多出了候選窗口,下圖中淺藍色窗體既是:
在處理上還是很簡陋,比如退格還不支持刪除輸入串,還不支持點擊上屏,等等。這些屬於業務邏輯的細節了,可以慢慢精耕細作。