输入法学习笔记
输入法学习
1 前言
输入法学习的例子是基于 sdk 下 samples\android-8\SoftKeyboard 例子学习,同时参考源码中 的 packages\inputmethods 下的三个输入法软件:LatinIME,OpenWnn,PinyinIME。
2 输入法整体
输入法整体结构由四个部分组成(四个类) ,分
别是:SoftKeyboard,LatinKeyboardView, LatinKeyboard,CandidateView。还有其他的一些 xml 格式配置文件(后文讨论) 1. SoftKeyboard:继承于 InputMethodService,包括输入法内部逻辑,键盘布局,选词等, 最终把选出的字符通过 commitText 提交出来 2. LatinKeyboard:实现词语匹配,负责解析并保存键盘布局,并提供选词算法,供程序 运行当中使用 3. LatinKeyboardView:专门用于绘制键盘外观,它与
CandidateView 合起来,组成了 InputView,就是我们看到的软键盘 4. CandidateView:候选词框,在中文输入时体现的尤为明显。
3 SoftKeyboard
3.1 概述
SoftKeyboard 类继承于 InputMethodService, 是一种特殊的 service, 安装到系统中后由 android 系统在管理输入法的时候统一调用,所有的输入法都继承于 InputMethodService 类以满足被 系统识别的需要。InputMethodService 作为一种 service 也有类似 onCreat(),onStart(), onFinish(),onDestory()等回调函数,只是形式上发生了变化。 不同于一般的 service,InputMethodService
有一些对于输入法来说独一无二的方法: 1. onInitializeInterface():这里是进行 UI 初始化的地方,创建以后和配置修改以后,都会 调用这个方法。源码中这个函数函数体为空,是需要继承类来实现的。 2. onStartInput():根据源码的提示,这个函数在 text 开始被编辑的时候调用,我们可以在 这里做一下初始化的工作来匹配产生编辑动作 editor。 3. onCreateInputView():返回一个层次性的输入视图,而且只是在第一次这个视图显示的 时候被调用。 4. onCreateCandidatesView():创建候选框的视图
5. onCreateExtractTextView():在全屏模式下的一个视图(正常不会调用) 6. onStartInputView(): 它是在输入视图被显示并且在一个新的输入框中输入已经开始的时 候调用,正是在这个方法中,将 inputview 和当前 keyboard 重新关联起来。 7. onFinishInput():用户停止编辑时调用 SoftKeyboard 的生命周期如下图:
3.2 源码解析
3.2.1 onCreate
private String mWordSeparators; //默认的使得输入中断的字符 @Override public void onCreate() { super.onCreate(); mWordSeparators = getResources().getString(R.string.word_separators); // 对 resource 这 个 东 西 有 了 一 些 了 解 : getResources 是 contextWrapper 类 的 函 数 , //contextWrapper
而是 inputmethodservice 的间接基类,这里的意思是把 } 在这里仅仅是把使得输入中断的字符提取出来,字符的设置在 strings.xml 文件里。
3.2.2 onInitializeInterface
private int mLastDisplayWidth; //记录键盘宽度 @Override public void onInitializeInterface() { //这只加载键盘,类似于 findViewById,离真正生成界面还早 if (mQwertyKeyboard != null) { //如果键盘已经加载了就进入这里 // Configuration changes can happen after the keyboard gets recreated, so we need to
be able to //re-build the keyboards if the available space has changed. int displayWidth = getMaxWidth();//获取屏幕的宽度以检查配置是否被更改 if (displayWidth == mLastDisplayWidth)//说明配置没有被更改 return;//没有被更改则没有动作 mLastDisplayWidth = displayWidth; //配置被更改了执行这里,记录下目前的宽度 } //然后重新
new 各个键盘 mQwertyKeyboard = new LatinKeyboard(this, R.xml.qwerty); mSymbolsKeyboard = new LatinKeyboard(this, R.xml.symbols); mSymbolsShiftedKeyboard = new LatinKeyboard(this, R.xml.symbols_shift); } 注意,在源码中 getMaxWidth()函数被如下诠释: /** * Return the maximum width,
in pixels, available the input method. * Input methods are positioned at the bottom of the screen and, unless * running in fullscreen, will generally want to be as short as possible * so should compute their height based on their contents. However, they *
can stretch as much as needed horizontally. The function returns to * you the maximum amount of space available horizontally, which you can * use if needed for UI placement. * * <p>In many cases this is not needed, you can just rely on the normal * view layout
mechanisms to position your views within the full horizontal
* space given to the input method. * * <p>Note that this value can change dynamically, in particular when the * screen orientation changes. */ public int getMaxWidth() { WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); return wm.getDefaultDisplay().getWidth();
}
3.2.3 onCreateInputView
@Override public View onCreateInputView() { //mInputView = (KeyboardView)findViewById(R.layout.input); //上边的函数 findViewById 对于 keyboardView 是不能用的只对 TextView 等可以用 mInputView = (KeyboardView) getLayoutInflater().inflate(R.layout.input, null); //加载 keyboardview
的资源 xml 文件 mInputView.setOnKeyboardActionListener(this); //加载监听函数,建类的时候已经加载了监听函数 mInputView.setKeyboard(mQwertyKeyboard);//默认加载字母键盘 return mInputView; //通过这个 return,自己定义的 keyboardview 类对象就与这个类绑定了 }
3.2.4 onCreateCandidatesView
private CompletionInfo[] mCompletions; //候选串之串 private StringBuilder mComposing = new StringBuilder(); //一个字符串 private boolean mPredictionOn; //这东西是决定能不能有候选条 private boolean mCompletionOn; //决定 auto 是否需要显示在候选栏 @Override public View onCreateCandidatesView()
{ mCandidateView = new CandidateView(this); //这里的构造函数为什么是 new 而不是像 //inputview 一样在资源里绑定呢? return mCandidateView; //通过这个 return,自己定义的 CandidatesView 就与这个类绑定了 }
3.2.5 onStartInput
/** * Called to inform the input method that text input has started in an * editor. You should use this callback to initialize the state of your * input to match the state of the editor given to it. * * @param attribute The attributes of the editor that input
is starting
* in. * @param restarting Set to true if input is restarting in the same * editor such as because the application has changed the text in * the editor. Otherwise will be false, indicating this is a new * session with the editor. */ @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; //允许 auto 的内容显示在后选栏中 //候选串字串清空 mCompletions = null; // We are now going to initialize our state
based on the type of text being edited. //一个靠谱的猜测: inputtype 的给定值里面有那么几个掩码, 但是从参数传来的具体 inputtype 值里面包含了所有的信息,不同的掩码能够得出不同的信息 //例如 TYPE_MASK_CLASS 就能得出下面四种,这四种属于同一类期望信息,这个信息叫 做 CLASS,下面一个掩码 TYPE_MASK_VARIATION 按位与出来的是一类 //叫做 VARIATION 的信息 switch (attribute.inputType&EditorInfo.TYPE_MASK_CLASS)
{ //按位与的两者是同一类型的,attribute 是 EditorInfo 类型的参数 case EditorInfo.TYPE_CLASS_NUMBER: case EditorInfo.TYPE_CLASS_DATETIME: // Numbers and dates default to the symbols keyboard, with no extra features. mCurKeyboard = mSymbolsKeyboard; //选择数字键盘 break; case EditorInfo.TYPE_CLASS_PHONE:
// Phones will also default to the symbols keyboard, though often you will want to have a dedicated phone keyboard. Symbols keyboard 就是数字键盘 mCurKeyboard = mSymbolsKeyboard; break; case EditorInfo.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 & EditorInfo.TYPE_MASK_VARIATION; if (variation == EditorInfo.TYPE_TEXT_VARIATION_PASSWORD ||variation EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD)
{ // Do not display predictions what the user is typing when they are entering a password. mPredictionOn = false; //密码框的输入是不需要候选词条的 } if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || variation == EditorInfo.TYPE_TEXT_VARIATION_URI || variation
== EditorInfo.TYPE_TEXT_VARIATION_FILTER) { // Our predictions are not useful for e-mail addresses or URIs. mPredictionOn = false; //如果是网站或者是邮箱地址,不用候选词条 } if ((attribute.inputType&EditorInfo.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(); //判断是否是全屏 //经过测试,当输入法处在全屏模式的时候,原本 auto 的候选词会显示在输入法的候选栏中 //这是 mCompletiOn 的作用,这个值初始化设为 false. //如果把这里的两个值都设置为 true 则可以发现再输入任意 auto 的时候都会在候选栏中显示 auto 的词语 } // 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); // 这 个 函 数 的 定 义 在 keyboard 文件中,稍后分析。根据输入目标设置回车键 } 注意,在这里要提一下 EditorInfo: EditorInfo 继承自 inputType,文中的所以标志位都是来源于 inputType: TYPE_CLASS_NUMBER Class for numeric text. TYPE_CLASS_DATETIME
Class for dates and times. TYPE_CLASS_PHONE Class for a phone number. TYPE_CLASS_TEXT Class for normal text. TYPE_TEXT_VARIATION_PASSWORD Entering a password TYPE_TEXT_VARIATION_VISIBLE_PASSWORD Entering a password, which should be visible to the user. TYPE_TEXT_VARIATION_EMAIL_ADDRESS
Entering an e-mail address. TYPE_TEXT_VARIATION_FILTER Entering text to filter contents of a list etc TYPE_TEXT_VARIATION_URL Entering a URI TYPE_TEXT_FLAG_AUTO_COMPLETE The text editor is performing auto-completion of the text being entered based on its own
semantics, which it will present to the user as they type.
3.2.6 onStartInputView
/** * Called when the input view is being shown and input has started on * a new editor. This will always be called after {@link #onStartInput}, * allowing you to do your general setup there and just view-specific * setup here. You are guaranteed that {@link
#onCreateInputView()} will * have been called some time before this function is called. * * @param info Description of the type of text being edited. * @param restarting Set to true if we are restarting input on the * same text field as before. */ @Override
public void onStartInputView(EditorInfo attribute, boolean restarting) { super.onStartInputView(attribute, restarting); // Apply the selected keyboard to the input view.
mInputView.setKeyboard(mCurKeyboard); mInputView.closing(); } 绑定键盘到 inputview
3.2.7 onFinishInput
/** * This is called when the user is done editing a field. * this to reset our state. */ @Override public void onFinishInput() { super.onFinishInput(); // Clear current composing text and candidates. mComposing.setLength(0); updateCandidates(); // We only
hide the candidates window when finishing input on // a particular editor, to avoid popping the underlying application // up and down if the user is entering text into the bottom of // its window. setCandidatesViewShown(false); mCurKeyboard = mQwertyKeyboard;
if (mInputView != null) { mInputView.closing(); //关闭键盘,具体函数体在 keyboardview 里 } } 简单的关闭候选窗口 We can use
3.2.8 onUpdateSelection
@Override public void onUpdateSelection(int oldSelStart, int oldSelEnd,int newSelStart, int newSelEnd,int candidatesStart, int candidatesEnd) { //光标! super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,candidatesStart, candidatesEnd); //当输入框向输入法报告用户移动了光标时调用。当用户移动输入框中的光标的时候,它就
默认的表示本次输入完成了, //然后将候选词以及正在输入的文本复位,并且向编辑器报告输入法已经完成了一个输入。 //四个整形都是坐标? // If the current selection in the text view changes, we should // clear whatever candidate text we have. if (mComposing.length() > 0 && (newSelStart != candidatesEnd|| newSelEnd !=
candidatesEnd)) { mComposing.setLength(0); //候选栏置空 updateCandidates(); //候选栏置空 InputConnection ic = getCurrentInputConnection(); //这个语句和下面 if 里面那个,决定 了结束输入的全过程 if (ic != null) { ic.finishComposingText(); //这个语句的作用是,让输入目标内的下划线去掉,完成一次 编辑 } } }
3.2.9 onDisplayCompletions
@Override onDisplayCompletions public void onDisplayCompletions(CompletionInfo[] completions) { //当需要在候选栏里面显示 auto 的内容 //猜测此函数作用: 当全屏幕模式的时候, mCompletionOn 置 true,可以通过候选栏来显示 auto if (mCompletionOn) { //必须这个变量允许 mCompletions = completions; //赋值给本来里面专门记录候选值的变量
if (completions == null) { setSuggestions(null, false, false); //如果没有候选词,就这样处置 return; } List<String> stringList = new ArrayList<String>(); for (int i=0; i<(completions != null ? completions.length : 0); i++) { CompletionInfo ci = completions[i]; if (ci !=
null) stringList.add(ci.getText().toString());//由 CompletionInfo 向 String 转变的过程 } setSuggestions(stringList, true, true); } } 个人理解:这个函数如果没有全屏显示模式的话应该是没用的
3.2.10 onKeyDown
@Override public boolean onKeyDown(int keyCode, KeyEvent event) { //这是重载了基类的,经测试确定, 只有在硬件盘被敲击时候才调用, 除了那个键本身的功效, 还有这里定义的这些是对输入法的影响 switch (keyCode) { case KeyEvent.KEYCODE_BACK: //这就是那个破箭头,扭曲的 // The InputMethodService already takes care of the back // key for
us, to dismiss the input method if it is shown.
// However, our keyboard could be showing a pop-up window // that back should dismiss, so we first allow it to do that. if (event.getRepeatCount() == 0 && mInputView != null) { //mInputView 类是自己定义的 keyBoardView 类 if (mInputView.handleBack()) { //通过 back 键来关闭键盘的元凶在这里
//这函数干吗呢?猜测:如果成功地荡掉了键盘,就返回真 return true; } } break; case KeyEvent.KEYCODE_DEL: // Special handling of the delete key: if we currently are // composing text for the user, we want to modify that instead // of let the application to the delete itself. if (mComposing.length()
> 0) { onKey(Keyboard.KEYCODE_DELETE, null); //所以,onkey 定义中的事情才是软键盘的事件 return true; } break; case KeyEvent.KEYCODE_ENTER: // Let the underlying text editor always handle these. return false; default: // For all other keys, if we want to do transformations
on // text being entered with a hard keyboard, we need to process // it and do the appropriate action. if (PROCESS_HARD_KEYS) { //这个是个废柴变量,因为在前面赋值了,永远是 true if (keyCode == KeyEvent.KEYCODE_SPACE && (event.getMetaState()&KeyEvent.META_ALT_ON) != 0) { //为什么有这个按位与?因为这个
META_ALT_ON 就是用来按位与来判断是否按下 alt //条件:alt+空格 // A silly example: in our input method, Alt+Space // is a shortcut for 'android' in lower case. InputConnection ic = getCurrentInputConnection(); if (ic != null) { // First, tell the editor that it is no longer in
the // shift state, since we are consuming this. ic.clearMetaKeyStates(KeyEvent.META_ALT_ON); // 清除组合键状态,如果不清除,出来的字符就不是 Android keyDownUp(KeyEvent.KEYCODE_A);
//由此可知,这些函数才是控制显示字符的,但貌似没那么简单 keyDownUp(KeyEvent.KEYCODE_N); keyDownUp(KeyEvent.KEYCODE_D); keyDownUp(KeyEvent.KEYCODE_R); keyDownUp(KeyEvent.KEYCODE_O); keyDownUp(KeyEvent.KEYCODE_I); keyDownUp(KeyEvent.KEYCODE_D); // And we consume this event. return true;
} } if (mPredictionOn && translateKeyDown(keyCode, event)) { return true; } } } return super.onKeyDown(keyCode, event); } 注意,这里要说一下 InputConnection: InputConnection 接口是接收输入的应用程序与 InputMethod 间的通讯通道。它可以完成以下 功能:如读取光标周围的文本,向文本框提交文本,向应用程序提交原始按键事件。 1. public abstract
boolean finishComposingText ():强制结束文本编辑器,无论联想输入 (composing text)是否激活。文本保持不变,移除任何与此文本的编辑样式或其他状态。 光标保持不变。 2. public abstract boolean clearMetaKeyStates (int states): 在指定的输入连接中清除指定的元 键(meta key)按下状态。
3.2.11 onKeyUp
@Override public boolean onKeyUp(int keyCode, KeyEvent event) { // If we want to do transformations on text being entered with a hard // keyboard, we need to process the up events to update the meta key // state we are tracking. if (PROCESS_HARD_KEYS) { //哈哈,判断是不在使用硬件输入要懂得,mete
keys 意味着 shift 和 alt 这类的键 if (mPredictionOn) { mMetaState = MetaKeyKeyListener.handleKeyUp(mMetaState,keyCode, event); //处理 matakey 的释放 } } return super.onKeyUp(keyCode, event); //只有在一个键被放起时候执行,但经过测试,他不是执行输入的,仅仅是再输入之前做些事
务, }
3.2.12 translateKeyDown
/** * This translates incoming hard key events in to edit operations on an * InputConnection. It is only needed when using the * PROCESS_HARD_KEYS option. */ private boolean translateKeyDown(int keyCode, KeyEvent event) { //这个函数在 OnKeyDown 中用到了 //这个是当组合键时候用,shift+A
或者别的 Alt+A 之类 mMetaState = MetaKeyKeyListener.handleKeyDown(mMetaState, keyCode, event); //处理 matakey 的按下, 猜测: 每一个 long 型的 mMetaState 值都代表着一个 meta 键组合值。 int c = event.getUnicodeChar(MetaKeyKeyListener.getMetaState(mMetaState)); //如果没这套组合键,就返回 0。这又是在干什么?猜测:每一个
mMetaState 值,对应着一 //个 unicode 值,这一步就是为了得到它, mMetaState = MetaKeyKeyListener.adjustMetaAfterKeypress(mMetaState); //重置这个元状态。当取得了 C 值之后,完全可以重置原来状态了,后面的语句不会出现任 何问题。上面这三行有点疑问 InputConnection ic = getCurrentInputConnection(); //后边这函数是 inputmethodservice 自己的,获得当前的链接
if (c == 0 || ic == null) { return false; } boolean dead = false; //一个 dead=true 意味着是一个有定义的组合键 if ((c & KeyCharacterMap.COMBINING_ACCENT) != 0) { //看看 c 所昭示的这个键能不能被允许组合键 dead = true; //定义下来看能否使用这个组合键 c = c & KeyCharacterMap.COMBINING_ACCENT_MASK; //这样就得到了真正的码值
} if (mComposing.length() > 0) { //这是处理“编辑中最后字符越变”的情况 char accent = mComposing.charAt(mComposing.length() -1 ); //返回正在编辑的字串的最后一个字符 int composed = KeyEvent.getDeadChar(accent, c); //这种情况下最后是返回了新的阿斯课码。composed 最终还是要还给 c.作为 onKey 的参数。 if (composed != 0) { c =
composed;
mComposing.setLength(mComposing.length()-1); // 要把最后一个字符去掉,才能够在下一步中越变成为新的字符 } } onKey(c, null); //强制输入 C,这样就实现了组合键的功效 return true; }
3.2.13 updateShiftKeyState
/** * Helper to update the shift state of our keyboard based on the initial * editor state. */ private void updateShiftKeyState(EditorInfo attr) { //但是,这个函数每次输入一个字母都要执行用于在开始输入前切换大写 //它首先是判断是否输入视图存在,并且输入框要求有输入法,然后根据输入框的输入类型 来获得是否需要大小写,最后定义在输入视图上。经测试,每当键盘刚出来的时候会有,每
输入一个字符都会有这个函数的作用 if (attr != null&& mInputView != null && mQwertyKeyboard == mInputView.getKeyboard()) { //getKeyboard 又是个可得私有变量的公有函数条件的含义是:当有字母键盘存在的时候 int caps = 0; EditorInfo ei = getCurrentInputEditorInfo(); //获得当前输入框的信息?本.java 中,大多数的 attr 参数于这个东西等同 if
(ei != null && ei.inputType != EditorInfo.TYPE_NULL) { //这个破 inputtype 类型是全 0,一般不会有这种破类型 caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType); //返回的东西不是光标位置,得到的是是否需要大写的判断,但是返回值是怎么弄的?? } mInputView.setShifted(mCapsLock || caps != 0); //参数 boolean
} }
3.2.14 keyDownUp
/** * Helper to send a key down / key up pair to the current editor. */ private void keyDownUp(int keyEventCode) { getCurrentInputConnection().sendKeyEvent( new KeyEvent(KeyEvent.ACTION_DOWN, keyEventCode)); getCurrentInputConnection().sendKeyEvent( new KeyEvent(KeyEvent.ACTION_UP,
keyEventCode));
//参见文档中 KeyEvent
//明白了,这个函数是用来特殊输出的,就好像前面定义的“android”输出,但如果简单地 从键盘输入字符,是不会经过这一步的 //一点都没错,强制输出,特殊输出,就这里 }
3.2.15 sendKey
/** * Helper to send a character to the editor as raw key events. */ private void sendKey(int keyCode) { //传入的参数是阿斯课码处理中断符的时候使用到了 switch (keyCode) { case '\n': keyDownUp(KeyEvent.KEYCODE_ENTER); //又是“特别输入”或称为“强制输入” break; default: if (keyCode >= '0' && keyCode
<= '9') { keyDownUp(keyCode - '0' + KeyEvent.KEYCODE_0); } else { getCurrentInputConnection().commitText(String.valueOf((char) keyCode), 1); } break; } } // Implementation of KeyboardViewListener //你难道没看见这个类定义时候的接口吗?那个接口定义的监听函数就是为了监听这种 On 事件的,这就是软键盘按压事件
3.2.16 onKey
// Implementation of KeyboardViewListener public void onKey(int primaryCode, int[] keyCodes) { if (isWordSeparator(primaryCode)) { //后面定义的函数当输入被中断符号中断 Handle separator if (mComposing.length() > 0) { commitTyped(getCurrentInputConnection()); } sendKey(primaryCode);
//提交完了输出之后,还必须要把这个特殊字符写上 updateShiftKeyState(getCurrentInputEditorInfo()); //看看是否到了特殊的位置,需要改变大小写状态 } else if (primaryCode == Keyboard.KEYCODE_DELETE) { handleBackspace(); } else if (primaryCode == Keyboard.KEYCODE_SHIFT) {
//但是硬键盘上面的这个好像没用,待考证大小写转换 handleShift(); } else if (primaryCode == Keyboard.KEYCODE_CANCEL) { //左下角那个键,关闭 handleClose(); return; } else if (primaryCode == LatinKeyboardView.KEYCODE_OPTIONS) { //这个键,是这样的,前面的 LatinKeyboardView 这个类里面定义了 EYCODE_OPTIONS //用来描述长按左下角关闭键的代替。经测试,千真万确
Show a menu or something' } else if (primaryCode == Keyboard.KEYCODE_MODE_CHANGE //就是显示着“abc”或者"123"的那个键 && mInputView != null) { Keyboard current = mInputView.getKeyboard(); if (current == mSymbolsKeyboard || current == mSymbolsShiftedKeyboard) { current
= mQwertyKeyboard; } else { current = mSymbolsKeyboard; } mInputView.setKeyboard(current); //改变键盘的根本操作,但是对于具体输入的是大写字母这件事情,还要等按下了之后在做 定论 if (current == mSymbolsKeyboard) { current.setShifted(false); //测试,这里要是设置为 true,打开之后只是 shift 键的绿点变亮,但是并没有变成另一个符 号键盘 } }
else { handleCharacter(primaryCode, keyCodes); //这就是处理真正的字符处理函数,不是那些其他的控制键 } }
3.2.17 isWordSeparator
public boolean isWordSeparator(int code) { String separators = getWordSeparators(); return separators.contains(String.valueOf((char)code)); }
3.2.18 sendKey
private void sendKey(int keyCode) { switch (keyCode) { case '\n': keyDownUp(KeyEvent.KEYCODE_ENTER); break; default:
if (keyCode >= '0' && keyCode <= '9') { keyDownUp(keyCode - '0' + KeyEvent.KEYCODE_0); } else { getCurrentInputConnection().commitText(String.valueOf((char) keyCode), 1); } break; } }
3.2.19 updateShiftKeyState
/** * Helper to update the shift state of our keyboard based on the initial * editor state. */ private void updateShiftKeyState(EditorInfo attr) { if (attr != null && mInputView != null && mQwertyKeyboard == mInputView.getKeyboard()) { int caps = 0; EditorInfo
ei = getCurrentInputEditorInfo(); if (ei != null && ei.inputType != EditorInfo.TYPE_NULL) { caps = getCurrentInputConnection().getCursorCapsMode(attr.inputType); } mInputView.setShifted(mCapsLock || caps != 0); } }
3.2.20 commitTyped
/** * Helper function to commit any text being composed in to the editor. */ private void commitTyped(InputConnection inputConnection) { if (mComposing.length() > 0) { inputConnection.commitText(mComposing, mComposing.length()); //后边的参数决定了光标的应有位置 mComposing.setLength(0);
updateCandidates(); //这两行联手,一般能造成候选栏置空与候选词条串置空的效果 } }
3.2.21 onText
public void onText(CharSequence text) { //这个函数是 keyboardview 向 service 传递一个字符串的函数 InputConnection ic = getCurrentInputConnection(); if (ic == null) return; ic.beginBatchEdit();
if (mComposing.length() > 0) { commitTyped(ic); } ic.commitText(text, 0); ic.endBatchEdit(); updateShiftKeyState(getCurrentInputEditorInfo()); }
3.2.22 updateCandidates
/** * Update the list of available candidates from the current composing * text. This will need to be filled in by however you are determining * candidates. */ private void updateCandidates() { //此函数处理的是不允许从 auto 获取的情况,应该是大多数情况 if (!mCompletionOn) { //mComposing
记录着候选字符串之串,待考证 if (mComposing.length() > 0) { ArrayList<String> list = new ArrayList<String>(); //字符串之串 list.add(mComposing.toString()); setSuggestions(list, true, true); } else { setSuggestions(null, false, false); } } }
3.2.23 setSuggestions
public void setSuggestions(List<String> suggestions, boolean completions,boolean typedWordValid) { //这第三个参数是前面函数调用的时候人为给的,没什么玄妙 if (suggestions != null && suggestions.size() > 0) { setCandidatesViewShown(true); //让候选栏可视 } else if (isExtractViewShown()) //疑问?
{ setCandidatesViewShown(true); //不止这个,很多地方都表明,当需要输入的程序处在全屏的时候,需要候选栏显示 } if (mCandidateView != null) //只有当有候选条的时候才显示{ mCandidateView.setSuggestions(suggestions, completions, typedWordValid); //就是改变了一下 suggestion,在 candidateView 里面真正靠的是 onDraw } }
3.2.24 handleBackspace
private void handleBackspace() {//删除一个字,用的就是他 final int length = mComposing.length(); if (length > 1) { mComposing.delete(length - 1, length); getCurrentInputConnection().setComposingText(mComposing, 1); updateCandidates(); } else if (length > 0) {//就是在说等于
1 的时候 mComposing.setLength(0); getCurrentInputConnection().commitText("", 0); updateCandidates(); } else { keyDownUp(KeyEvent.KEYCODE_DEL); } updateShiftKeyState(getCurrentInputEditorInfo()); //看看是否需要重归大写状态(例如又到达了行首) }
3.2.25 handleShift
private void handleShift() { //这才是大小写的切换,是正常切换(通过转换键) if (mInputView == null) { return; } Keyboard currentKeyboard = mInputView.getKeyboard(); if (mQwertyKeyboard == currentKeyboard) { // Alphabet keyboard checkToggleCapsLock(); //只有当键盘是字母键盘的时候,需要检验锁(控制变幻频率,不能过快)
mInputView.setShifted(mCapsLock || !mInputView.isShifted()); //关键语句 } else if (currentKeyboard == mSymbolsKeyboard) { mSymbolsKeyboard.setShifted(true); mInputView.setKeyboard(mSymbolsShiftedKeyboard); mSymbolsShiftedKeyboard.setShifted(true); } else if (currentKeyboard
== mSymbolsShiftedKeyboard) { mSymbolsShiftedKeyboard.setShifted(false); //所谓的 setShift,仅仅指的是那个键盘的大小写键变化,经测试,只要 android:code=-1 就有这 种绿点效果不用 setShift()都行 mInputView.setKeyboard(mSymbolsKeyboard); //这才是真正的 shift 工作(换成另外一个键盘) mSymbolsKeyboard.setShifted(false);
} }
3.2.26 handleCharacter
private void handleCharacter(int primaryCode, int[] keyCodes) { //primayCode 是键的阿斯课码值 if (isInputViewShown()) { if (mInputView.isShifted()) { primaryCode = Character.toUpperCase(primaryCode); //这才真正把这个字符变成了大写的效果,经测试,没有就不行 //把键盘换成大写的了还不够,那只是从 View 上解决了问题,一定要这样一句才行
} } if (isAlphabet(primaryCode) && mPredictionOn) { //输入的是个字母,而且允许候选栏显示 mComposing.append((char) primaryCode); //append(添加)就是把当前的输入的一个字符放到 mComposing 里面来 getCurrentInputConnection().setComposingText(mComposing, 1); //在输入目标中也显示最新得到的 mComposing. updateShiftKeyState(getCurrentInputEditorInfo());
//每当输入完结,都要检验是否需要变到大写 updateCandidates(); } else { getCurrentInputConnection().commitText(String.valueOf((char) primaryCode), 1); //比如说当输入的是“ ‘”这个符号的时候,就会掉用这个 //结果就是 remove 掉所有编辑中的字符,第二个参数的正负,决定着 //光标位置的不同 } }
3.2.27 handleClose
private void handleClose() { //关闭键盘件的作用就在这里,左下角那个.,记住!!!左下角那个,不是弯钩键!! !! !! commitTyped(getCurrentInputConnection()); requestHideSelf(0); //关掉输入法的区域,这才是关闭的王道.似乎这句包含了上面那句的作用(测试结果) mInputView.closing(); //这个函数不懂什么意思待问?? 哪里都测试,哪里都没有用处?? }
3.2.28 checkToggleCapsLock
private void checkToggleCapsLock() { //记录上次变幻的时间 long now = System.currentTimeMillis(); if (mLastShiftTime + 800 > now) { //不允许频繁地换大小写?
mCapsLock = !mCapsLock; mLastShiftTime = 0; } else { mLastShiftTime = now; } }
4 LatinKeyboard
4.1 概述
LatinKeyboard 是继承自 sdk 的 Keyboard 类,负责解析并保存键盘布局,并提供选词算法, 供程序运行当中使用。其中键盘布局是以 XML 文件存放在资源当中的。比如我们在汉字输 入法下,按下 b、a 两个字母。LatinKeyboard 就负责把这两个字母变成爸、把、巴等显示在 CandidateView 上。
4.2 源码解析
LatinKeyboard 的源码相对较少,大部分的工作都由 sdk 的 Keyboard 完成了。
4.2.1 createKeyFromXml
@Override protected Key createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser) { //描绘键盘时候自动调用??是不是在此类的构造函数中就使用了?肯定 是自己的函数调用这个函数。 Key key = new LatinKey(res, parent, x, y, parser); if (key.codes[0] == 10) { //重载的目的,仅仅是为了记录回车键的值而已(以
Key 型记录)然后下一个函数要用到 mEnterKey = key; //无非就是想对回车键做改观 } return key; } 注意,Keyboard 中的这个函数是: protected Key createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser) { return new Key(res, parent, x, y, parser); } 只是产生了一个新的键。那么 LatinKey
又不同于 sdk 中的 key 类,那么 LatinKey 是哪来的 呢。我们发现如下的类
4.2.2 LatinKey
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);//用的是老一辈的构造函数 }
/** * Overriding this method so that we can reduce the target area for the key that * closes the keyboard. */ @Override public boolean isInside(int x, int y) { return super.isInside(x, codes[0] == KEYCODE_CANCEL ? y -10: y); //只有一个左下角 cancel 键跟 super 的此函数不一样,其余相同
//仅仅为了防止错误的点击?将 cancel 键的作用范围减小了 10,其余的,如果作用到位,都 返回 true } }
4.2.3 setImeOptions
前面提到过的一个函数 /** * This looks at the ime options given by the current editor, to set the * appropriate label on the keyboard's enter key (if it has one). */ void setImeOptions(Resources res, int options) { //在 SoftKeyboard 的 StartInput 函数最后用到了传入了 EditorInfo.imeOptions
类型的 options 参数。此变量地位与 EditorInfo.inputType 类似。但作用截然不同 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; //把图片设为空,并不代表就是空,只是下面的 Lable 可以代替之 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: mEnterKey.icon = res.getDrawable( R.drawable.sym_keyboard_return); mEnterKey.label = null; break;
} }
5 自建工程演练
5.1 Manifest
输入法与其他的 service 有不同之处就在于他接受系统对于输入法的调用。从 manifest 我们 可以看出: <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> </application> </manifest> 1. 关键点 1: android:permission="android.permission.BIND_INPUT_METHOD">
这里获取一个权限是关于绑定到 input_method, 这样这个输入法才会接受系统传递来的有关 输入的细节,表明这是个输入法服务。 2. 关键点 2: <intent-filter> <action android:name="android.view.InputMethod" /> </intent-filter> 这里的 intent-filter 是关键,他只接收"android.view.InputMethod"的调用,我的理解是在系统 需要调用输入法的时候(比如有人点击 edittext)系统会自动寻找相应
inputmethod 过滤的组 件,届时就会找到这个服务程序了。 3. 关键点 3: <meta-data android:name="android.view.im" android:resource="@xml/method" />
这是一个 html 的标签,通过 name 为 android.view.im 的 meta-data 来描述该输入法的一些属 性,meta-data 引用的是一个 XML 文件,该文件是输入法的配置文件,用来配置一些信息, 例如是否为默认输入法,是否具有配置 Activity 来配置输入法的一些选项,如果指定了配置 Activity 则在系统设置界面中的输入法设置中可以启动该 Activity 来设置输入法的配置项。
5.1.1 工程初步
根据 manifest,实际上系统就可以识别到你的输入法了,我们在 setting/language&keyboard 设置里可以发现我们做的输入法:
勾选了就可以在选择输入法的时候看到我们的输入法了,只不过我们还没有具体的代码实 现。我们的代码只有: package com.fang.MyKeyBoard; import android.inputmethodservice.InputMethodService; public class MyKeyBoard extends InputMethodService { }
5.2 删减例程
我的想法是在 sample softkeyboard 例子基础上进行简化,找到输入法最简约的形式,这样对 于理解输入法的基本概念有帮助。因此我复制了源码,并且做了删减。 删减内容包括以下几部分
5.2.1 键盘布局
键盘布局。键盘由原来的 4 行简化为现在的 3 行,功能键只保留删除和回车,以简化功能。 删减后的键盘外观如下:
改变方法: 在 res/xml 文件夹下的 qwerty.xml 文件标识了键盘的布局,改变这里的内容就可以改变键盘 布局。这里要注意的问题: 代码的键值在这里原则上是不建议更改的,因为这里的键值对应的都是 ASCII 码,在函数 handleCharacter(int primaryCode, int[] keyCodes)里会将 ASCII 码转换成相应的字符。如果要 自定义键盘及键盘码,并自己完成转换,可以更改。
5.2.2 Onkeydown
这个函数以及 onkeyup 都是为了响应硬键盘上的按键信息而作的特殊处理, 我们可以不在简 易键盘上实现这些东西。 改变方法:注销这两个函数。 同时我们发现还有一个 onkeydownup 的函数,这个其实就是模拟硬键盘向系统发了一个按 键指令,在做回车和删除这样的功能指令的时候我们需要,所以要留着。
5.2.3 OnText
这个函数是 keyboardview 向 service 传递一个字符串的函数,我们不用特别处理,因为我们 改变输入字码是通过 onkeydownup 模拟硬键盘来实现的。把函数方法体注释掉就可以。