android之輸入法

輸入法的例子和源代碼看瞭不少時間瞭,看得頭很暈,很鬱悶。靜下心來把整個代碼想瞭一遍,其實大部分代碼都在處理繪制界面,以及事件的處理,核心代碼很少,都被底層封裝得很完美瞭。
先看看一般輸入法的界面:

 
分為兩個部分,上部分是候選窗口(CandidateView),用來顯示候選詞,現在常用的輸入法都有這個功能,如在搜狗、google輸入法中輸入abc,輸入框中就會顯示很多相關聯的中文詞。下部分就是軟鍵盤瞭,這個沒什麼好說的。

輸入法中核心類是InputMethodService
其次就是:CandidateView 和KeyboardView
 
CandidateView為候選窗口,InputMethodService在啟動過程中會調用接口public View onCreateCandidatesView() ,在這個方法
中把CandidateView對象返回後,InputMethodService內部會將其佈局到相應的位置。
在android中沒有CandidateView父類,得自己從頭寫,一般的做法是:
通過方法public void setService(InputMethodService listener) 將Service類傳進來,然後再通過public void setSuggestions(List<String> suggestions…) 方法將候選詞列表傳遞過來,CandidateView將其顯示到界面上,用戶選擇結束後,再通過service的方法pickSuggestionManually(mSelectedIndex) ,將選擇的候選詞在列表中的序號傳遞回去。至此, CandidateView 就完成瞭它神聖的使命。
 
android中KeyboardView有一個通用類,繼承它可以通過簡單的配置文件就顯示出很專業軟鍵盤。在源代碼中,它絕大部分代碼都在做繪制工作和事件處理,不過就其本質功能來說是相當地簡單,用戶摁下軟鍵盤上的某個鍵後,它把這個鍵所代表的值傳遞給InputMethodService類也完成瞭它的使命。InputMethodService在public View onCreateInputView() 方法中獲得該View。
 
InputMethodService就是輸入法的核心瞭,該類是一個Service,跟其它默默無聞的Service不同的是,它是一個帶有View的Service。其內部有幾個個重要的接口:
InputMethodImpl
InputMethodSessionImpl
InputConnection
InputMethodService通過這幾個個類跟系統和輸入框進行交互的。
輸入框從InputMethodService獲取信息是通過InputConnection來實現的, 在啟動輸入法時,InputConnection由客戶端控件創建,並傳遞給輸入法應用,由輸入法應用調用,進行信息反饋

InputMethod接口定義瞭一套操縱輸入法應用的方法。如,bindInput, hideInput, startInput等。為瞭系統安全,這類接口隻有系統可以訪問,客戶端控件無法直接調用這個接口。所有的輸入法應用都需要客戶端控件具有 BIND_INPUT_METHOD權限,作為系統的安全機制,否則將無法與輸入法服務交互。

InputMethodSession作為InputMethod的輔助接口類,為客戶端控件開放瞭可直接調用的函數接口。包括向輸入法應用分發鍵盤事件,更新光標位置,更新編輯區域內選擇的問題信息等。客戶端控件通過IIputMethodSession對於輸入法應用的交互是單向的,即隻能向輸入法應用傳遞信息,無法獲取信息
 
以上幾個點是從網上copy過來的,感覺這幾點對於理解InputMethodService特別有用。
代碼看得太多反而看不清本質,這幾個類中最實用的是InputConnection的
public boolean commitText(CharSequence text, int newCursorPosition) 。
通過KeyboardView和CandidateView, InputMethodService類已經獲得瞭想要的內容,然後通過這個方法把值傳遞給輸入框。
 
按奈不住心中讀源代碼的鬱悶,先來寫一個輸入法發泄一下:
先來一個CandidateView,設想的佈局如下:

 
這個View中不進行任何自繪制,用android現有的View,兩邊各一個按鈕(Button),用來滾動多個候選詞,中間顯示候選詞(TextView),為瞭方便CandidateView繼承RelativeLayout的內部類,便於加入子控件和控制,setService和 setSuggestions兩個方法可以不用,反正是內部類,不過為瞭配合上面的說明,思量再三還是加上瞭:
 
public class helloIme extends InputMethodService {
 
class CandidateView extends RelativeLayout{
    TextView tv;                 // 中間顯示候選詞
    Button btLeft, btRight; // 左右按鈕
    helloIme listener;         // helloIme 用於返回選中的 候選詞下標
    List<String> suggestions; // 候選詞列表, KeyboardView 不同的鍵按下後會設置相關的列表
    int mSelectedIndex = -1;  // 當前 候選詞下標
   
public CandidateView(Context context) {
    super(context);
   
    tv = new TextView(context);
    tv.setId(1);
    RelativeLayout.LayoutParams lpCenter = new RelativeLayout.LayoutParams(200, ViewGroup.LayoutParams.WRAP_CONTENT);
    lpCenter.addRule(RelativeLayout.CENTER_IN_PARENT);
    addView(tv, lpCenter);
    tv.setOnClickListener(new OnClickListener(){
        public void onClick(View v) {
            listener.pickSuggestionManually(mSelectedIndex);
        }
    });
   
    btLeft = new Button(context);
    btLeft.setText("<");
    btLeft.setOnClickListener(new OnClickListener(){
        public void onClick(View arg0) {
            mSelectedIndex = mSelectedIndex > 0 ? (mSelectedIndex – 1) : 0;
            tv.setText(suggestions.get(mSelectedIndex));
        }
    });
   
    RelativeLayout.LayoutParams lpLeft = new RelativeLayout.LayoutParams(60, ViewGroup.LayoutParams.WRAP_CONTENT);
    lpLeft.addRule(RelativeLayout.LEFT_OF, 1);
    addView(btLeft, lpLeft);
   
    btRight = new Button(context);
    btRight.setText(">");
    btRight.setOnClickListener(new OnClickListener(){
        public void onClick(View v) {
            mSelectedIndex = mSelectedIndex >= suggestions.size() – 1 ? suggestions.size() – 1 : mSelectedIndex + 1;
            tv.setText(suggestions.get(mSelectedIndex));
        }
    });
   
    RelativeLayout.LayoutParams lpRight = new RelativeLayout.LayoutParams(60, ViewGroup.LayoutParams.WRAP_CONTENT);
    lpRight.addRule(RelativeLayout.RIGHT_OF, 1);
    addView(btRight, lpRight);
}

public void setService(helloIme listener){
    this.listener = listener;
}

public void setSuggestions(List<String> suggestions) {
    mSelectedIndex = 0;
    tv.setText(suggestions.get(mSelectedIndex));
    this.suggestions = suggestions;
}
}
上面最重要的是粗體的那兩行,View的佈局還是花費瞭很多代碼:(
 
KeyboardView的佈局預想如下:

就兩個按鈕,點if時往輸入框中輸出if(){}, if(){}else if(){}…,whie時往輸入框中輸出whie(){},這個類同樣是繼承於RelativeLayout的內部類:
class KeyboardView extends RelativeLayout{
  public KeyboardView(Context context) {
        super(context);
       
        Button btIf = new Button(context);
        btIf.setText("if");
        btIf.setId(1);
        RelativeLayout.LayoutParams lpIf = new RelativeLayout.LayoutParams(100, 50);
        lpIf.addRule(RelativeLayout.CENTER_HORIZONTAL);
       
        btIf.setOnClickListener(new OnClickListener(){
            public void onClick(View v) {
                setCandidatesViewShown(true); // 顯示 CandidateView
                helloIme.this.onKey("if"); // 將點擊按鈕的值傳回給 InputMethodService
            }
        });
        addView(btIf, lpIf);
       
        Button btWhile = new Button(context);
        btWhile.setText("while");
        RelativeLayout.LayoutParams lpWhile = new RelativeLayout.LayoutParams(100, 50);
        lpWhile.addRule(RelativeLayout.BELOW , 1);
        lpWhile.addRule(RelativeLayout.ALIGN_LEFT, 1);
       
        btWhile.setOnClickListener(new OnClickListener(){
            public void onClick(View v) {
                setCandidatesViewShown(true);
                helloIme.this.onKey("while");
            }
        });
        addView(btWhile, lpWhile);
    }
}
 
CandidateView默認是不顯示的,所以需要調用InputMethodService 的setCandidatesViewShown()方法。
 
接下來把helloIme的代碼貼出來:
public class helloIme extends InputMethodService {
    private List<String>suggestionlist; // 當前候選詞表
    private Hashtable<String, List<String>> data; // 詞典數據
    private KeyboardView mkeyView;
    private CandidateView mCandView;
   
    public void onInitializeInterface() { //InputMethodService在啟動時,系統會調用該方法,具體內容下回再表
        // 初始化 詞典數據
        data = new Hashtable<String, List<String>>();
        List<String> list = new ArrayList<String>();
        list.add("if(){}");
        list.add("if(){}else if(){}");
        list.add("if(){}else{}");
        data.put("if", list);
       
        list = new ArrayList<String>();
        list.add("while(){}");
        data.put("while", list);
    }
   
    public View onCreateInputView() {
        mkeyView = new KeyboardView(this);
        return mkeyView;
    }

    public View onCreateCandidatesView() {
        mCandView = new CandidateView(this);
        mCandView.setService(this);
        return mCandView;
    }
   
    public void pickSuggestionManually(int mSelectedIndex){
        getCurrentInputConnection().commitText(suggestionlist.get(mSelectedIndex), 0); // 往輸入框輸出內容
        setCandidatesViewShown(false); // 隱藏 CandidatesView
    }
   
    public void onKey(CharSequence text){
        // 根據按下的按鈕設置候選詞列表
        suggestionlist = data.get(text);
        mCandView.setSuggestions(suggestionlist);
    }

    class KeyboardView extends RelativeLayout{
     //……
    }

    class CandidateView extends RelativeLayout{
    //……
    }
}
 
代碼寫完,再來寫配置文件,
在res目錄下面建立一個新目錄xml,然後創建一個method.xml
<?xml version="1.0" encoding="utf-8"?>
<!– The attributes in this XML file provide configuration information –>
<!– for the Search Manager. –>

<input-method
    xmlns:android="http://schemas.android.com/apk/res/android" />
設置Manifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="test.helloIme"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <service android:name="helloIme"
        android:permission="android.permission.BIND_INPUT_METHOD">
            <intent-filter>
                <action android:name="android.view.InputMethod"/>
            </intent-filter>
            <meta-data android:name="android.view.im" android:resource="@xml/method"/>
        </service>
    </application>
    <uses-sdk android:minSdkVersion="5" />

</manifest>
直接運行程序,eclipse輸出如下Log:
[2010-08-25 17:16:48 – helloIme]Installing helloIme.apk…
[2010-08-25 17:16:50 – helloIme]Success!
[2010-08-25 17:16:50 – helloIme]/helloIme/bin/helloIme.apk installed on device
[2010-08-25 17:16:50 – helloIme]Done!

嗯,安裝成功瞭!呵呵,革命尚未成功,還需在模擬器上進行設置:
點擊settings->Language & keyboard,在下部出現瞭一個test,右邊有個checkbox,選上它。
找一個有輸入框的應用,最簡單到寫短消息的畫面,左鍵長按輸入框,會彈出一個輸入法選擇提示框,點進去就會看到剛才創建的輸入法瞭,點擊右邊的單選框,oh,my ime,漂亮的hello輸入法就展現在面前瞭:

 
 
android果然很強大啊。

 

摘自 Change everyday

發佈留言