点击Finish,完成项目的创建,可以看到项目工程结构如下:
在Android SDK 2.3.3模拟器上运行本Sample,需要在Setting中选择使用本Sample,需要在Language&keyboard中选中本Sample的名称。
当尝试选中Sample Soft Keyboard时,Android会出现安全提示。IME的确要选择自己信任的,因为它可以收集和记录所有你的输入,这个特性如果被有心人利用会很恐怖。
选中Sample Soft Keyboard作为我们的输入法之后,进入需要输入法的地方,这里以短信界面作为范例,在输入框中长按,会出现“编辑文本”选单,点击“输入法”即可进入当前输入界面的输入法选择框。就可以使用输入法切换到本输入法看到它的keyboard。
之后就可以看到Soft keyboard键盘如下:
三、配置和资源文件解析
除去源代码将在后文统一分析之外,这里介绍下配置和资源文件。
1. AndroidMainifest.xml
每个Android应用都会有的配置描述文件。在这里,Sample把自己声明成了服务,而且绑定在了输入法之上。它的intent-filter是直接用的InputMethod接口,这也是所有的输入法的接口。
2. res目录
放置resource,即资源文件,里面蛮多东西的,具体如下。
(1) drawable目录,放置的是图标文件。
(2) values目录,包含strings.xml以及一些自定义的类型和值的xml文件。
strings.xml
― ime_name 定义了该输入法的名字
― word_separators 词的分隔符,即输入过程中可能用来表示一个词输入完成的符号,比如空格,标点等等)
― label_xx_key 为软键盘定义确认键的标签。在后面代码解析中可以看到,程序会根据输入框的信息来设置EnterKey的图标或者标签。如:在一个网址上面输入,就会显示一个搜索的图标,而在编辑短信时,如果在收信人写,那么EnterKey就是Next标签,用来直接跳到短信正文部分。
dimens.xml,定义软键盘的尺寸信息,包括键高(key_height),候选词字体的高度(candidate_font_height),候选词垂直间隙(candidate_vertical_padding)。
color.xml,定义候选词的背景颜色,比如正常(candidate_normal),推荐(candidate_recommended),背景(candidate_background)和其它(candidate_other)等颜色。
(3) layout目录,保存布局配置文件。这里只有一个配置文件:input.xml,它定义的是输入视图的信息,包括id(android:id="@+id/keyboard"),放置在屏幕下方(android:layout_alignParentBottom="true"),水平最大填充(android:layout_width="match_parent"),垂直包含子内容(android:layout_height="wrap_content")。
(4) xml目录,文件如下:
method.xml,为搜索管理提供配置信息。
qwerty.xml,英文字符的全键盘布局文件。定义很直观,很容易就可以看懂。
symbols_shift.xml和symbols.xml,是标点字符的全键盘布局文件。
四、源代码解析
(一)概述
从InputMethodServiceSample项目可以看出实现一个输入法至少需要CandidateView, LatinKeyboard, LatinKeyboardView,SoftKeyboard这四个文件:
CandidateView负责显示软键盘上面的那个候选区域。
LatinKeyboard负责解析并保存键盘布局,并提供选词算法,供程序运行当中使用。其中键盘布局是以XML文件存放在资源当中的。比如我们在汉字输入法下,按下b、a两个字母。LatinKeyboard就负责把这两个字母变成爸、把、巴等显示在CandidateView上。
LatinKeyboardView负责显示,就是我们看到的按键。它与CandidateView合起来,组成了InputView,就是我们看到的软键盘。
SoftKeyboard继承了InputMethodService,启动一个输入法,其实就是启动一个InputMethodService,当SoftKeyboard输入法被使用时,启动就会启动SoftKeyboard这个Service。
(二)LatinKeyboard.java
软键盘类,直接继承了Keyboard类,并定义一个xml格式的Keyboard的布局,来实现一个输入拉丁文的键盘。这里只是创建一个键盘对象,并不对具体的布局给出手段。
为了更好的理解LatinKeyboard类,这里简单介绍一下Keyboard类。Keyboard可以载入一个用来显示键盘布局的xml来初始化自己,并且可以保存这些键盘的键的属性。他有三个构造函数:
Keyboard(Context context, int xmlLayoutResId),用语境和xml资源id索引xml文件来创建。
Keyboard(Context context, int xmlLayoutResId, int modeId),这个和上面差不多,只不过多了一个modeld。
Keyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding),这个比较复杂,用一个空xml布局模板创建一个键盘,然后用指定的characters按照从左往右,从上往下的方式填满这个模板。
本文件源码前面完全继承keyboard,直接用了父类构造函数进行初始化。
这里因为重写了Keyboard类的createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser),为了要返回一个Key对象,干脆直接创建LatinKey对象好了。从这里我们能看出面向对象和使用框架的要求。
接着,本文件重载了一个createKeyFromXml的函数,这是一个回调函数,它在键盘描绘键的时候调用,从一个xml资源文件中载入一个键,并且放置在(x,y)坐标处。它还判断了该键是否是回车键,并保存起来。在这里,为了要返回一个Key对象,于是直接创建内部类的LatinKey对象。从这里我们能看出面向对象和使用框架的要求。
此外,还有一个函数是:setImeOptions,它是根据编辑框的当前信息,来为这个键盘的回车键设置适当的标签。输入框的不同,会产生不同的回车键的label或者icon。在这个函数中,有一个技巧是用了一些imeOption的位信息,比如IME_MASK_ACTION等等。主要是查看的EditorInfo的Action信息,这里有:
IME_ACTION_GO: go操作,将用户带入到一个该输入框的目标的动作。确认键将不会有icon,只有label: GO
IME_ACTION_NEXT: next操作,将用户带入到该文本框的写一个输入框中。如: 编辑短消息的时候,内容就是收件人手机号码框的next文字域。它也只是一个NEXT label就行了。
IME_ACTION_SEARCH: search操作,默认动作就是搜索。如: 在URL框中输入的时候,默认的就是search操作,它提供了一个像放大镜一样的icon。
IME-ACTION_SEND: send操作,默认动作就是发送当前的内容。如: 短消息的内容框里面输入的时候,后面通常就是一个发送操作。它也是只提供一个Label:SEND
DEFAULT: 默认情况下表示文本框并没有什么特殊的要求,所以只需要设置return的icon即可。
最后,它还定义了一个内部类——LatinKey,它直接继承了Key,来定义一个单独的键,它唯一重载的函数是isInside(int x , int y ),用来判断一个坐标是否在该键内。它重载为判断该键是否是CANCEL键,如果是则把Y坐标减少10px,按照他的解释是用来还原这个可以关掉键盘的键的目标区域。
(三)LatinKeyboardView.java
这里就是个View,自然也继承自View,因为前面创建的键盘只是一个概念,并不能实例出来一个UI,所以需要借助于一个VIEW类来进行绘制。这个类简单的继承了KeyboardView类,然后重载了一个动作方法,就是onLongPress。
它在有长时间按键事件的时候会调用,首先判断这个按键是否是CANCEL键,如果是的话就通过调用 KeyboardView被安置好的OnKeyboardActionListener对象,给键盘发送一个OPTIONS键被按下的事件。它是用来屏蔽CANCEL键,然后发送了一个未知的代码的键。
(四)CandidateView.java
CandidateView是一个候选字显示view,它提供一个候选字选择的视图,直接继承于View类即可。在我们输入字符时,它应该能根据字符显示一定的提示,比如拼音同音字啊,联想的字啊之类的。
1. 先看它定义了那些重要变量:
mService: candidateView的宿主类,即该view是为什么输入法服务的。
mSuggestions: 建议。比如说当我们输入一些字母之后输入法希望根据输入来进行联想建议。
mSelectedIndex: 用户选择的词的索引。
mSelectionHighlight: 描绘选择区域高亮的类。
mTypedWordValid: 键入的word是否合法正确。
mBgPadding: 背景填充区域。
mWordWidth: 每个候选词的宽度。
mWordX:每个候选词的X坐标。有了这两个变量,就能够在屏幕上准确的绘制出该候选键。
mColor*:定义了各种颜色。
mPaint: 一个绘图类,后面会用到
mVerticalPadding: 垂直填充区域。
mTargetScrollX: 目标滚动的横坐标,即要将目标滚动到何处。
mTotalWidth: 总的宽度
mGestureDetector: 声明一个手势监测器
GestureDetector对象似乎很少见,让我们了解一下android.view.GestureDetector。这是一个与动作事件相关的类,可以用来检测各种动作事件,这里称之为:手势监测器。它的回调函数是GestureDetector.OnGestureListener,在动作发生时执行,而且只能在触摸时发出,用滚动球无效。要使用这个通常要先建立一个对象,如同代码里体现的,然后设置GestureDetector.OnGestureListener 同时在 onTouchEvent(MotionEvent)中写入动作发生要执行的代码。
2. 构造函数,主要是对一些变量的初始化工作。
首先初始化了mSelectionHighlight,这是一个drawable对象,并利用drawable的setState方法设置这个drawable的初始状态。同时在res目录下加入一个color.xml文件来定义用到的所有颜色资源,然后用R索引,这些资源可以被加入到自己的R.java的内容里,可以直接引用。剩下的内容就是初始化背景,选中,未选中时的view的背景颜色,这里都是在前面color.xml内定义的了。用这样的方式获得:
Resources r = context.getResources();
获得当前资源对象的方法。
setBackgroundColor(r.getColor(R.color.candidate_background));
然后初始化了一个手势检测器(gesturedetector),它的Listener重载了一个方法,就是onScroll,这个类是手势检测器发现有scroll动作的时候触发。在这个函数里,主要是进行滑动的判断。
这里用到了很多view下的方法:getScrollX();getWidth();scrollTo(sx, getScrollY());invalidate();我们分别解释如下:
getScrollX():获得滚动后view的横坐标
scrollTo():滚动到目标坐标
getScrollY():获得滚动后view的纵坐标
invalidate():使view重画
在这里,distanceX是上次调用onscroll后滚动的X轴距离。假设这个view之前没有被滚动过,第一次滚动且坐标在显示区域内,sx=getScrollX()+distanceX,则view就scrollTo这个位置。如果sx超过了最大显示宽度,则scrollTo就滚想原先sx处,也就是不动。也就是说:系统滚动产生一个惯性的感觉,当你把view实际到了X坐标点,系统再给你加一个distanceX,这个distanceX不是两个动作之间的距离,应该是上一个滚动动作的停止点和本次滚动动作的停止点之间的距离,这个距离系统自己算,我们不用管,只要到了最大边界,view就不再滚动,或者说是原地滚动。
接下来:
setHorizontalFadingEdgeEnabled(true);// 设置view在水平滚动时,水平边是否淡出。
setWillNotDraw(false);// view不自己绘制自己
setHorizontalScrollBarEnabled(false);// 不设置水平滚动条
setVerticalScrollBarEnabled(false);// 不设置垂直滚动条
3. setService是设置宿主输入法。
4. computeHorizontalScrollRange,表示这个view的水平滚动区域,返回的是候选视图的总体宽度。
5. onMeasure,重载自view类,在布局阶段被父视图所调用。比如当父视图需要根据其子视图的大小来进行布局时,就需要回调这个函数来看该view的大小。当调用这个函数时必须在内部调用setMeasureDimension来对宽和高进行保存,否则将会有异常出现。这里重载它是为了系统检测要绘制的字符区的大小,因为字体可能有大小,应根据字体来。它首先计算自己的期望的宽度,调用resolveSize来看是否能够得到50px的宽度;然后是计算想要的高度,根据字体和显示提示区的padding来确定。
6. onDraw,view的主要函数,每个view都必须重写这个函数来绘制自己。它提供了一块画布,如果为空,则直接调用父类来画。
在这里的内部逻辑大概如下:
判断是否有候选词,没有的话就不用绘制。
初始化背景的填充区域,直接view的背景中得到即可。
对于每一个候选词,得到其文本,然后计算其宽度,然后再加上两边的空隙。
判断是否选择了当前词:触摸的位置+滚动了的位置。如果是在当前词的左边到右边之间,则将高亮区域绘制在画布上面,高亮区域设置的大小即为当前词的大小,并且保存被选词的索引。
将文本绘制在这个候选词的画布上面,它进行了一个判断,判断哪个才是推荐词。默认情况下是候选词的第一个词,但是它判断第一个词是否是合法的,如果是,则第一个词是候选词,否者第二个词才是候选粗,然后进行绘制。
绘制一条线,来分割各个候选词。上面提到的总共的宽度在所有的词都绘制出来之后,就能够得到了。
判断目标滚动是否是当前的,不是就需要滚动过去。
7. scrollToTarget,滚到到目标区域。得到当前值,然后加上一个滚动距离,看是否超过并进行相应调整,之后滚动到相应坐标。
8. setSuggestions,设置候选词,之后进行绘制。
9. onTouchEvent,触摸事件产生时调用。首先判断是否为gesturedetector监听的动作,如果不是就进行下面处理。初始化动作,把发生的动作记录下来,点触的坐标也记录下来。然后,根据动作类型分类反应:
向下:没动作;
移动:如果是向左移动就要手动的选择候选词;
向上:需要手动选择候选词。
10. takeSuggestionAt,选择在坐标x处的词,这个处理的是用户轻轻点击键盘,也就是选择候选词。
11. removeHighlight,去除高亮显示。
(五)SoftKeyboard.java
整个输入法的总体的框架,包括什么时候创建,什么时候显示输入法,和怎样和文本框进行通讯等等。上面的文件,都是为了这个类服务的。总体来说,一个输入法需要的是一个输入视图,一个候选词视图,还有一个就是和应用程序的链接。
基本时序图如下:
输入法在Android中的本质就是一个Service,假设用户刚刚启动Android,用户移动焦点首次进入文本编辑框时,Android便会通知Service开始进行初始化工作。于是便有了如图中的一系列动作。
追根溯源,onCreate方法继承至Service类,其意义和其他Service的是一样的。Sample在这里,做了一些非UI方面的初始化,即字符串变量词汇分隔符的初始化。
接下来执行onInitializeInterface,这里是进行UI初始化的地方,创建以后和配置修改以后,都会调用这个方法。Sample在这里对Keyboard进行了初始化,从XML文件中读取软键盘信息,封装进Keyboard对象。
第三个执行的就是onStartInput方法,在这里,我们被绑定到了客户端,接收所有关于编辑对象的详细信息。
第四个执行的方法是onCreateInputView,在用户输入的区域要显示时,这个方法由框架调用,输入法首次显示时,或者配置信息改变时,该方法就会被执行。在该方法中,对inputview进行初始化:读取布局文件信息,设置onKeyboardActionListener,并初始设置 keyboard。
第五个方法是onCreateCandidatesView,在要显示候选词汇的视图时,由框架调用。和onCreateInputView类似。在这个方式中,对candidateview 进行初始化。
第六个方法,也是最后一个方法,即onStartInputView,正是在这个方法中,将inputview和当前keyboard重新关联起来。
在上面的六个方法中,onCreateInputView和onCreateCandidatesView两个方法只有在初始化时才会执行一次,除非有配置信息发生改变。那么究竟什么是配置信息发生改变呢?在看InputMethodService的API文档时,可以看到有一个方法onConfigurationChanged,根据文档解释,这个方法主要负责配置更改的情况。在示例中,其没有override这个方法,但是在android源码包中的PinyinIME中,有使用这个方法,有兴趣的朋友可以在看完SoftKeyboard Sample之后,看看PinyinIME的源码。
关于本类中其它的一些方法,由于比较直观,就不进行讲解了,感兴趣的朋友可以参考《android sdk中 softkeyboard的自己解析(4)》。
五、输入法调试
通过使用调试模式加断点的方式,有助于我们更好的理解输入法的时序和每个类及其方法的功能和调用持续。
这里使用Eclipse的DDMS透视图进行调试,具体介绍参考《用Eclipse开发和调试Android应用程序》
首先切换到DDMS模式,在这个模式下面,DDMS将链接到正在运行的手机或模拟器,并且能够提取手机上面的各种信息,比如线程,还有各个正在后台运行的服务等等。点击工具条上的“Debug selected Process”,就能够将调试器植入到这个服务上面。
之后切换到debug模式,就会发现调试器已经链接到了这个模拟器,然后就可以像调试普通的程序一样调试这个输入法了。
通过debug模式,我们可以发现,输入法首先执行的onCreateInputView-> onCreateCandidatesView,而在这个时候,这个输入法的界面一点儿都还没有显现出来。当我们在一个输入框中点击鼠标时,系统会产生一个事件,最开始就被输入法捕获,然后再将控制权交给这个输入法。另外,切换对象的时候,输入法总是认为是一次输入的结束,然后进行一系列的reset工作。所有的键盘等事件,都会首先传递给输入法,所以,如果一个按键事件不是我们所能够处理的问题,我们需要将这个事件继续传递下去,而不要丢弃了,因为这可能是别的控件的事情。
在发送消息的界面,在输入完TO某人之后,点击content输入框,首先调用的是onFinishInput,也就是结束上一次的输入,准备这次的输入。之后调用的是onStartInputView,让界面显示出来。接着调用onStartInput,表示开始正式的输入。在这过程中,要完成根据不同的输入框,选择不同的键盘,当你输入一个键,首先触发的是onKey回调,在这里要判断是输入的普通字符,还是控制性的字符,比如删除,返回等等。比如这里输入一个 'g',然后会调用处理普通字符的函数handleCharacter。这里的策略就是,输入一个普通字符,就将Composing增加,并且更新这个候选词的列表。这里有一个很微妙的开关,就是mPrediction,它就是判断是否是需要保存这个Composing。在比如说URL框中输入的时候,就会置这个开关为关,直接将键入的输入到文本框中去。
为了测试所有的函数,你必须想出一种输入方式,让每个函数你都能执行到,那你就能够看清楚输入法的本来面目。
请各位朋友自己试试,对阅读和理解源代码的流程、时序和生命周期很有好处。也可以方便的找到自己的代码的bug。
六、输入法的调用
希望从一个View上调用输入法和接收输入法传过来的字符串,可以通过调用EditText这个widget。但是,如果要做出很炫很个性的输入法,就必须自己去和EditText一样连接输入法,介绍如下:
首先,定义一个继承自BaseInputConnection的类。前文提到过,输入法是通过commitText来提交选中字符。
public class MyBaseInputConnection extends BaseInputConnection{
public MyBaseInputConnection(View targetView, boolean fullEditor) {
super(targetView, fullEditor);
}
public static String tx="";
//输入法程序就是通过调用这个方法把最终结果输出来的
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
tx = text.toString();
return true;
}
}
BaseInputConnection相当于一个InputMethodService和View之间的一个通道。每当InputMethodService产生一个结果时,都会调用BaseInputConnection的commitText方法,把结果传递出来。
之后,采用如下方式,呼出输入法,并且把自定义的BaseInputConnection通道传递给InputMethodService。
public class MyView extends XXView ...{
//得到InputMethodManager
InputMethodManager input = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
//定义事件处理器
ResultReceiver receiver = new ResultReceiver(new Handler() {
public void handleMessage(Message msg) {
}
});
...
//在你想呼出输入法的时候,调用这一句
input.showSoftInput(this, 0, mRR);
...
@Override
//这个方法继承自View。把自定义的BaseInputConnection通道传递给InputMethodService
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return new MyBaseInputConnection(this, false);
}
}
低级界面上面,自己调用输入法并接收输入法的输出结果,就是这样的。
更多相关文章,请访问:
http://blog.sina.com.cn/deaboway
http://blog.csdn.net/deaboway
以上两个blog同步更新。