本文主要介绍android自带输入法实例SoftKeyboard的源码,共分为两篇:第一篇为SoftKeyboard框架概述,第二篇为源码注释。
1、IMF简介
一个IMF结构中包含三个主要的部分:
- input method manager:管理各部分的交互。它是一个客户端API,存在于各个应用程序的context中,用来沟通管理所有进程间交互的全局系统服务。
- input method(IME):实现一个允许用户生成文本的独立交互模块。系统绑定一个当前的输入法。使其创建和生成,决定输入法何时隐藏或者显示它的UI。同一时间只能有一个IME运行。
- client application:通过输入法管理器控制输入焦点和IME的状态。一次只能有一个客户端使用IME。
1.1 InputManager
由UI控件(View,TextView,EditText等)调用,用来操作输入法。比如,打开,关闭,切换输入法等。
它是整个输入法框架(IMF)结构的核心API,处理应用程序和当前输入法的交互。可以通过Context.getSystemService()来获取一个InputMethodManager的实例。
在开发过程中,最基础最重要的就是养成阅读API的习惯。优秀的程序员要养成把自己关在小黑屋里,断绝与外界的联网和联系,仅仅靠自己电脑中的开发环境和API文档,以及漂亮女仆送来的每天三顿饭,写出优秀的程序。这个在武侠小说中叫闭关,在软件开发中叫Clean Room,哈哈。
Android的API文档在:%SDK_ROOM%/docs/reference/index.html,
InputManager类的位置:%SDK_ROOM%/docs/reference/android/view/inputmethod/InputMethodManager.html
由于,该类跟本次要讲的Sample关系不大,这里就不详细分析,请各位自行阅读API doc吧。
1.2 InputMethodService
包括输入法内部逻辑,键盘布局,选词等,最终把选出的字符通过commitText提交出来。实现输入法的基础就是名为InputMethodService的类,比如你要实现一个谷歌输入法,就是要extends本类。我们接下来要学习的SoftKeyboard Sample也是extends本类。InputMethodService类的位置在:%SDK_ROOM%/docs/reference/android/inputmethodservice/InputMethodService.html
- InputMethodService是InputMethod的一个完整实现,你可以再在其基础上扩展和定制。它的主要方法如下:
- onInitializeInterface() 顾名思义,它在初始化界面的时候被调用,而一般是由于配置文件的更改导致该函数的执行
- onBinndInput() 它在另外的客户端和该输入法连接时调用
- onStartInput() 非常重要的一个回调,它在编辑框中用户已经开始输入的时候调用。比如,当点击一个输入框,我们需要根据这个输入框的信息,设置输入法的一些特性,这个在Sample中很有体会。
- onCreateInputView() 返回一个层次性的输入视图,而且只是在这个视图第一次显示的时候被调用
- onCreateCandidatesView() 同onCreateInputView(),只不过创建的是候选框的视图。
- onCreateExtractTextView() 比较特殊,是在全屏模式下的一个视图。
- onStartInputView() 在输入视图被显示并且在一个新的输入框中输入已经开始的时候调用。
基本上输入法的定制,都是围绕在这个类来实现的,它主要提供的是一个基本的用户界面框架(包括输入视图,候选词视图和全屏模式),但是这些都是要实现者自己去定制的。这里的实现是让所有的元素都放置在了一个单一的由InputMethodService来管理的窗口中。它提供了很多的回调API,需要我们自己去实现。一些默认的设置包括:
- 软键盘输入视图,它通常都是被放置在屏幕的下方。
- 候选词视图,它通常是放置在输入视图的上面。
当我们输入的时候,需要改变应用程序的界面来适应这些视图的放置规则。比如在Android上面输入,编辑框会自动变形腾出一个软键盘的位置来。
两个非常重要的视图:
1. 软输入视图。是与用户交互的主要发生地:按键,画图或者其他的方式。通常的实现就是简单的用一个视图来处理所有的工作,并且在调用 onCreateInputView()的时候返回一个新的实例。通过调用系统的onEvaluateInputViewShow()来测试是否需要显示输入视图,它是系统根据当前的上下文环境来实现的。当输入法状态改变的时候,需要调用updateInputViewShown()来重新估计一下。
2. 候选词视图。当用户输入一些字符之后,输入法可能需要提供给用户一些可用的候选词的列表。这个视图的管理和输入视图不大一样,因为这个视图是非常的短暂的,它只是在有候选词的时候才会被显示。可以用setCandidatesViewShow()来设置是否需要显示这个视图。正是因为这个显示的频繁性,所以它一般不会被销毁,而且不会改变当前应用程序的视图。
最后,关于文本的产生,这是一个IME的最终目的。它通过InputConnection来链接IME和应用程序的:能够直接产生想要的按键信息,甚至直接在候选和提交的文本中编辑。当用户在不同的输入目标之间切换的时候,IME会不断的调用onFinishInput() 和 onStartInput()。在这两个函数中,需要反复做的就是复位状态,并且应对新的输入框的信息。
以上是一个输入法的最基本的介绍,下面将根据Sample中的SoftKeyboard来说明这些问题。
2、创建Eclipse工程
这里使用最新版本的Android SDK 2.3.3下的SoftKeyboard Sample来创建工程,其实,从1.5版本,该Sample就已经存在了。同时,由于SoftKeyboard会使人误解为KeyBoard的子类,这里特别改名为InputMethodServiceSample,更符合其功能和特性。
点击Finish,完成项目的创建,可以看到项目工程结构如下:
在Android SDK 2.3.3模拟器上运行本Sample,需要在Setting中选择使用本Sample,需要在Language&keyboard中选中本Sample的名称。
当尝试选中Sample Soft Keyboard时,Android会出现安全提示。IME的确要选择自己信任的,因为它可以收集和记录所有你的输入,这个特性如果被有心人利用会很恐怖。
选中Sample Soft Keyboard作为我们的输入法之后,进入需要输入法的地方,这里以短信界面作为范例,在输入框中长按,会出现“编辑文本”选单,点击“输入法”即可进入当前输入界面的输入法选择框。就可以使用输入法切换到本输入法看到它的keyboard。
3、配置和资源文件解析
除去源代码将在后文统一分析之外,这里介绍下配置和资源文件。
3.1 AndroidMainifest.xml
每个Android应用都会有的配置描述文件。在这里,Sample把自己声明成了服务,而且绑定在了输入法之上。它的intent-filter是直接用的InputMethod接口,这也是所有的输入法的接口。
3.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,是标点字符的全键盘布局文件。
4、源代码解析
4.1 概述
从InputMethodServiceSample项目可以看出实现一个输入法至少需要CandidateView, LatinKeyboard, LatinKeyboardView,SoftKeyboard这四个文件:
- CandidateView负责显示软键盘上面的那个候选区域。
- LatinKeyboard负责解析并保存键盘布局,并提供选词算法,供程序运行当中使用。其中键盘布局是以XML文件存放在资源当中的。比如我们在汉字输入法下,按下b、a两个字母。LatinKeyboard就负责把这两个字母变成爸、把、巴等显示在CandidateView上。
- LatinKeyboardView负责显示,就是我们看到的按键。它与CandidateView合起来,组成了InputView,就是我们看到的软键盘。
- SoftKeyboard继承了InputMethodService,启动一个输入法,其实就是启动一个InputMethodService,当SoftKeyboard输入法被使用时,启动就会启动SoftKeyboard这个Service。
4.2LatinKeyboard.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,按照他的解释是用来还原这个可以关掉键盘的键的目标区域。
4.3 LatinKeyboardView.java
这里就是个View,自然也继承自View,因为前面创建的键盘只是一个概念,并不能实例出来一个UI,所以需要借助于一个VIEW类来进行绘制。这个类简单的继承了KeyboardView类,然后重载了一个动作方法,就是onLongPress。
它在有长时间按键事件的时候会调用,首先判断这个按键是否是CANCEL键,如果是的话就通过调用 KeyboardView被安置好的OnKeyboardActionListener对象,给键盘发送一个OPTIONS键被按下的事件。它是用来屏蔽CANCEL键,然后发送了一个未知的代码的键。
4.4 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,去除高亮显示。
4.5 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开发实例详解之IMF(Android SDK Sample—SoftKeyboard)