1 功能需求
- 触屏设备主界面中有一个文本编辑框,底部区域固定显示一个数字键盘,键盘中除数字键外,还带有*和#键功能;
- 提供一个自定义的数字输入法,生成apk安装包文件,嵌入到img镜像文件中去。
2 设计思路
- 借鉴Android SDK目录下Sample中的 SoftKeyboard这个例子,参考demo演示,模仿实现IME(Input Method Editor);
- 在平台版本Android 1.5、SDK版本3、版本名称CUPCAKE版本之后,Android就开放它的IMF(Input Method Framework),让我们能够开发自己的输入法。而开发输入法最好的入手例子就是SoftKeyboard,虽然这个例子只是简单的英文和数字等的输入,但是它本身也算是写得非常清楚和完整的输入法实现。
3 源码分析
3.1 生命周期
接下来我们介绍一些比较重要的生命周期方法
onCreate()
输入法创建过程时,首先调用该方法进行输入法组件的主要初始化,该方法只会调用一次。
onCreateInputView()
当需要去生成用于创建输入的视图时,会调用此方法。在首次显示你的输入法时,以及每次由于配置更改而需要重新创建输入法时将被调用。
onCreateCandidateView()
当需要去生成用于显示候选键的视图时,会调用此方法,类似onCreateInputView()方法。
onStartInputView()
当输入视图正在显示并且在新的编辑框上开始输入时被调用。这将始终在onStartInput()方法之后被调用,允许你在此处进行常规设置(设置空格键Icon,并刷新整个键盘视图)和仅进行特定视图的设置(将所选键盘应用于输入视图)。你应该保证onCreateInputView方法在该方法被调用之前调用。
onCurrentInputMethodSubtypeChanged()
子类型改变时被调用。设置空格键Icon,并刷新整个键盘视图。
onFinishInput()
用户完成字段编辑后,将调用此方法。我们可以使用它来重置我们的状态。比如清除当前的已输入文字和候选内容,还有控制候选键显示区域的可见性。默认情况下它是隐藏的。
onDestroy()
输入法服务结束时调用,只调用一次,在方法中做好资源释放的工作。
3.2 实现原理
- 自定义输入法相关的类,都放在com.example.android.softkeyboard包的下面
- 在AndroidMainifest.xml中声明输入法组件
在清单文件中声明输入法服务,以及申请绑定输入法权限。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.softkeyboard">
<application android:label="@string/ime_name">
<service android:name="SoftKeyboard"
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>
<activity android:name=".ImePreferences" android:label="@string/settings_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
</activity>
</application>
</manifest>
- CandidateView.java
CandidateView类是一个候选键的视图类,它直接继承于 View类即可。在用户输入字符时,它应该能根据字符显示一定的提示,比如拼音同音字,联想的字之类的情况。
类中主要的方法如下
/**
* 是设置宿主输入法,用于回到服务的连接以与文本字段进行通信。
*
* @param listener
*/
public void setService(SoftKeyboard listener) {
mService = listener;
}
/**
* 设置候选词,之后进行绘制候选键视图。
* @param suggestions
* @param completions
* @param typedWordValid
*/
public void setSuggestions(List<String> suggestions, boolean completions,
boolean typedWordValid) {
clear();
if (suggestions != null) {
mSuggestions = new ArrayList<String>(suggestions);
}
mTypedWordValid = typedWordValid;
scrollTo(0, 0);
mTargetScrollX = 0;
// Compute the total width
onDraw(null);
invalidate();
requestLayout();
}
/**
* 移除高亮显示
*/
private void removeHighlight() {
mTouchX = OUT_OF_BOUNDS;
invalidate();
}
- LatinKeyboard.java
软键盘类,直接继承了 Keyboard类,来实现一个输入拉丁文的键盘。
类中主要的私有变量如下
/**
* 存储更改模式键的当前状态。它的宽度将会动态更新,当变为不可见时来匹配这个区域。
*/
private Key mModeChangeKey;
/**
* 存储语言切换键的当前状态(例如 地球键),当返回true时就应该可见。当这个key变得不可见时,它的宽度将会缩小为零。
*/
private Key mLanguageSwitchKey;
/**
* 当mLanguageSwitchKey可见时,存储大小和mModeChangeKey的其他信息。
* 这应该是不可变的,当改变mLanguageSwitchKey的可见性时,仅用作参考大小。
*/
private Key mSavedModeChangeKey;
/**
* 当mLanguageSwitchKey可见时,存储大小和mLanguageSwitchKey的其他信息。
* 这应该是不可变的,当改变mLanguageSwitchKey的可见性时,仅用作参考大小。
*/
private Key mSavedLanguageSwitchKey;
类中主要的方法如下
/**
* 动态改变语言切换键的可见性(例如 全球键)
*
* @param visible
*/
void setLanguageSwitchKeyVisibility(boolean visible) {
if (visible) {
// 语言切换键应该显示。使用保存的布局来恢复模式变化键和语言切换键的大小。
mModeChangeKey.width = mSavedModeChangeKey.width;
mModeChangeKey.x = mSavedModeChangeKey.x;
mLanguageSwitchKey.width = mSavedLanguageSwitchKey.width;
mLanguageSwitchKey.icon = mSavedLanguageSwitchKey.icon;
mLanguageSwitchKey.iconPreview = mSavedLanguageSwitchKey.iconPreview;
} else {
// 语言切换键应该隐藏。改变模式变化键的宽度去填充语言键的空间,用户不会看到任何奇怪的间隙。
mModeChangeKey.width = mSavedModeChangeKey.width + mSavedLanguageSwitchKey.width;
mLanguageSwitchKey.width = 0;
mLanguageSwitchKey.icon = null;
mLanguageSwitchKey.iconPreview = null;
}
}
/**
* 查看当前编辑器提供的IME选项,在键盘Enter键设置合适的标签(如果有一个)。
*
* @param res
* @param options
*/
void setImeOptions(Resources res, int options) {
if (mEnterKey == null) {
return;
}
switch (options & (EditorInfo.IME_MASK_ACTION | EditorInfo.IME_FLAG_NO_ENTER_ACTION)) {
case EditorInfo.IME_ACTION_GO:
mEnterKey.iconPreview = null;
mEnterKey.icon = null;
mEnterKey.label = res.getText(R.string.label_go_key);
break;
case EditorInfo.IME_ACTION_NEXT:
mEnterKey.iconPreview = null;
mEnterKey.icon = null;
mEnterKey.label = res.getText(R.string.label_next_key);
break;
case EditorInfo.IME_ACTION_SEARCH:
mEnterKey.icon = res.getDrawable(R.drawable.sym_keyboard_search);
mEnterKey.label = null;
break;
case EditorInfo.IME_ACTION_SEND:
mEnterKey.iconPreview = null;
mEnterKey.icon = null;
mEnterKey.label = res.getText(R.string.label_send_key);
break;
default:
// 设置回车键Icon
mEnterKey.icon = res.getDrawable(R.drawable.sym_keyboard_return);
mEnterKey.label = null;
break;
}
}
/**
* 它还定义了一个内部类,叫做LatinKey,它直接继承了Key,来定义一个单独的键,
* 它唯一重载的函数是isInside(int x , int y ),用来判断一个坐标是否在该键内。
* 它重载为判断该键是否是CANCEL键,如果是则把Y坐标减少10px,
* 按照他的解释是用来还原这个可以关掉键盘的键的目标区域。
*/
static class LatinKey extends Keyboard.Key {
public LatinKey(Resources res, Keyboard.Row parent, int x, int y,
XmlResourceParser parser) {
super(res, parent, x, y, parser);
}
/**
* 重写此方法,以便我们可以减少关闭键盘键的目标区域。
*
* @param x
* @param y
*/
@Override
public boolean isInside(int x, int y) {
return super.isInside(x, codes[0] == KEYCODE_CANCEL ? y - 10 : y);
}
}
- LatinKeyboardView.java
因为前面定义的LatinKeyboard这个类来说就只是一个概念而已,并不能实例出来一个UI,所以需要借助于一个View类来进行绘制。这个类简单的继承了KeyboardView类,然后重载了一个方法,就是onLongPress方法。 - SoftKeyboard.java
编写软键盘输入法的示例.这段代码注重于简单性而不是完整性,因此绝对不应将其视为完整的软键盘实现其目的是提供一个基础示例,说明如何开始编写输入方法,并在适当时充实。IME其实就是一Service,用户在点击文本编辑框输入字符时,便会按照输入法生命周期顺序执行。整个输入法包括什么时候建,什么时候显示输入法,和怎样和文本框进行通讯等等。上面的几个.java文件,都是为了这个类服务的。总体来说,一个输入法需要的是一个输入视图,一个候选词视图,还有一个就是和应用程序的链接。
其类中主要的方法如下,从上到下顺序执行
/**
* 输入法组件的主要初始化。一定要调用父类InputMethodService的onCreate()方法。
* 用来创建输入法管理对象和字符串分隔符的初始化。
*/
@Override
public void onCreate() {
super.onCreate();
mInputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
mWordSeparators = getResources().getString(R.string.word_separators);
}
/**
* 这是你可以进行所有UI初始化的地方。在创建和任何配置更改后将调用这个方法。
* 在这里对Keyboard进行了初始化,从XML文件中读取软键盘信息,封装进Keyboard对象。
*/
@Override
public void onInitializeInterface() {
if (mQwertyKeyboard != null) {
// 重新创建键盘后,可能会发生配置更改,因此,如果可用空间已更改,我们需要能够重新构建键盘。
int displayWidth = getMaxWidth();
if (displayWidth == mLastDisplayWidth) return;
mLastDisplayWidth = displayWidth;
}
mQwertyKeyboard = new LatinKeyboard(this, R.xml.qwerty);
mSymbolsKeyboard = new LatinKeyboard(this, R.xml.symbols);
mSymbolsShiftedKeyboard = new LatinKeyboard(this, R.xml.symbols_shift);
}
/**
* 这是我们初始化输入方法以开始在应用程序上进行操作的要点。
* 在这里,我们已经绑定到客户端,并且现在正在接收有关我们的编辑对象的所有详细信息。
*
* @param attribute
* @param restarting
*/
@Override
public void onStartInput(EditorInfo attribute, boolean restarting) {
super.onStartInput(attribute, restarting);
// Reset our state. We want to do this even if restarting, because
// the underlying state of the text editor could have changed in any way.
mComposing.setLength(0);
updateCandidates();
if (!restarting) {
// Clear shift states.
mMetaState = 0;
}
mPredictionOn = false;
mCompletionOn = false;
mCompletions = null;
// We are now going to initialize our state based on the type of
// text being edited.
switch (attribute.inputType & InputType.TYPE_MASK_CLASS) {
case InputType.TYPE_CLASS_NUMBER:
case InputType.TYPE_CLASS_DATETIME:
// Numbers and dates default to the symbols keyboard, with
// no extra features.
mCurKeyboard = mSymbolsKeyboard;
break;
case InputType.TYPE_CLASS_PHONE:
// Phones will also default to the symbols keyboard, though
// often you will want to have a dedicated phone keyboard.
mCurKeyboard = mSymbolsKeyboard;
break;
case InputType.TYPE_CLASS_TEXT:
// This is general text editing. We will default to the
// normal alphabetic keyboard, and assume that we should
// be doing predictive text (showing candidates as the
// user types).
mCurKeyboard = mQwertyKeyboard;
mPredictionOn = true;
// We now look for a few special variations of text that will
// modify our behavior.
int variation = attribute.inputType & InputType.TYPE_MASK_VARIATION;
if (variation == InputType.TYPE_TEXT_VARIATION_PASSWORD ||
variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) {
// Do not display predictions / what the user is typing
// when they are entering a password.
mPredictionOn = false;
}
if (variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
|| variation == InputType.TYPE_TEXT_VARIATION_URI
|| variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
// Our predictions are not useful for e-mail addresses
// or URIs.
mPredictionOn = false;
}
if ((attribute.inputType & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0) {
// If this is an auto-complete text view, then our predictions
// will not be shown and instead we will allow the editor
// to supply their own. We only show the editor's
// candidates when in fullscreen mode, otherwise relying
// own it displaying its own UI.
mPredictionOn = false;
mCompletionOn = isFullscreenMode();
}
// We also want to look at the current state of the editor
// to decide whether our alphabetic keyboard should start out
// shifted.
updateShiftKeyState(attribute);
break;
default:
// For all unknown input types, default to the alphabetic
// keyboard with no special features.
mCurKeyboard = mQwertyKeyboard;
updateShiftKeyState(attribute);
}
// Update the label on the enter key, depending on what the application
// says it will do.
mCurKeyboard.setImeOptions(getResources(), attribute.imeOptions);
}
/**
* 在用户输入的区域要显示时,调用这个方法,输入法首次显示时,或者配置信息改变时,该方法就会被执行。
* 在该方法中,对InputView进行初始化:读取布局文件信息,设置onKeyboardActionListener,并初始设置 keyboard。
*/
@Override
public View onCreateInputView() {
mInputView = (LatinKeyboardView) getLayoutInflater().inflate(
R.layout.input, null);
mInputView.setOnKeyboardActionListener(this);
setLatinKeyboard(mQwertyKeyboard);
return mInputView;
}
/**
* 在需要显示候选词汇的视图时,调用这个方法。和onCreateInputView()这个方法类似。
* 在这个方法中,对CandidateView进行初始化。
*/
@Override
public View onCreateCandidatesView() {
mCandidateView = new CandidateView(this);
mCandidateView.setService(this);
return mCandidateView;
}
/**
* 当显示输入视图并且在新的文本编辑框上开始输入时调用,将InputView和当前Keyboard重新关联起来。
* 这将始终在onStartInput()之后调用,允许你在此处进行常规设置,而在此处仅进行特定视图的设置。
* 你可以保证在调用此函数之前一定时间会调用onCreateInputView()。
*
* @param attribute 对要编辑的文本类型的描述
* @param restarting 如果我们要像以前一样在相同的文本字段上重新开始输入,请设置为true。
*/
@Override
public void onStartInputView(EditorInfo attribute, boolean restarting) {
super.onStartInputView(attribute, restarting);
// Apply the selected keyboard to the input view.
setLatinKeyboard(mCurKeyboard);
mInputView.closing();
final InputMethodSubtype subtype = mInputMethodManager.getCurrentInputMethodSubtype();
mInputView.setSubtypeOnSpaceKey(subtype);
}
在上面的六个方法中,onCreateInputView()和onCreateCandidatesView()两个方法只有在初始化时才执行一次,除非有配置信息发生改变。