天天看點

Android學習 - 自定義輸入法

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

Android學習 - 自定義輸入法

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

輸入法中核心類是InputMethodService,其次就是:CandidateView和KeyboardView。

CandidateView為候選視窗,InputMethodService在啟動過程中會調用接口publicView onCreateCandidatesView(),在這個方法中把CandidateView對象傳回後,InputMethodService内部會将其布局到相應的位置。

在android中沒有CandidateView父類,得自己從頭寫,一般的做法是:

通過方法public voidsetService(InputMethodService listener)将Service類傳進來,然後再通過publicvoid 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通過這幾個類跟系統和輸入框進行互動的。

1、輸入框從InputMethodService擷取資訊是通過InputConnection來實作的,在啟動輸入法時,InputConnection由用戶端控件建立,并傳遞給輸入法應用,由輸入法應用調用,進行資訊回報。

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

3、InputMethodSession作為InputMethod的輔助接口類,為用戶端控件開放了可直接調用的函數接口。包括向輸入法應用分發鍵盤事件,更新光标位置,更新編輯區域内選擇的問題資訊等。用戶端控件通過IIputMethodSession對于輸入法應用的互動是單向的,即隻能向輸入法應用傳遞資訊,無法擷取資訊。

以上幾個點是從網上copy過來的,感覺這幾點對于了解InputMethodService特别有用。代碼看得太多反而看不清本質,這幾個類中最實用的是InputConnection的。

public boolean commitText(CharSequence text, intnewCursorPosition)
           

通過KeyboardView和CandidateView,InputMethodService類已經獲得了想要的内容,然後通過這個方法把值傳遞給輸入框。

先來一個CandidateView,設想的布局如下:

Android學習 - 自定義輸入法

這個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) {
					<strong>listener.pickSuggestionManually(mSelectedIndex);</strong>
				}
			});

			btLeft = new Button(context);
			btLeft.setText("<");
			btLeft.setOnClickListener(new OnClickListener() {
				public void onClick(View arg0) {
					mSelectedIndex = mSelectedIndex > 0 ? (mSelectedIndex - 1)
							: 0;
					<strong>tv.setText(suggestions.get(mSelectedIndex));</strong>
				}
			});

			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的布局預想如下:

Android學習 - 自定義輸入法

就兩個按鈕,點if時往輸入框中輸出if(){},if(){}else if(){},while時往輸入框中輸出while(){},這個類同樣是繼承于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;

	// InputMethodService在啟動時,系統會調用該方法,具體内容下回再表
	public void onInitializeInterface() { 
		// 初始化 詞典資料
		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 CandidateView extends RelativeLayout {
		TextView tv; // 中間顯示候選詞
		Button btLeft, btRight; // 左右按鈕
		helloIme listener; // helloIme 用于傳回選中的 候選詞下标
		// 候選詞清單, KeyboardView 不同的鍵按下後會設定相關的清單
		List<String> suggestions; 
		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;
		}
	}

	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);
		}
	}
}
           

代碼寫完,再來寫配置檔案,在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,在AndroidManifest.xml中加入:

<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>
           

直接運作程式,eclipse輸出如下Log:

[2015-05-15 14:40:48 - Test006] Installing Test006.apk...
[2015-05-15 14:40:50 - Test006] Success!
[2015-05-15 14:40:50 - Test006] \Test006\bin\Test006.apk installed on device
[2015-05-15 14:40:50 - Test006] Done!
           

安裝成功了,還需在模拟器上進行設定:

點選settings->Language & keyboard,在下部出現了一個test006,右邊有個checkbox,選上它。并設定預設輸入法為test006。

找一個有輸入框的應用,最簡單到寫短消息的畫面,左鍵長按輸入框,會彈出一個輸入法選擇提示框,點進去就會看到剛才建立的輸入法了,點選右邊的單選框,漂亮的hello輸入法就展現在面前了:

Android學習 - 自定義輸入法

demo