最近工作比较忙,加班出差,因此更新慢了些。今天刚好有空,写一篇实例详解吧。
本博前面的文章介绍了Android开发环境的搭建和模拟器的常用操作。本次,将以Android Sample中经典的SoftKeyboard项目为例,详细解析Android上一个小型项目的开发过程和注意事项。
从SDK 1.5版本以后,Android就开放它的IMF(Input Method Framework),让我们能够开发自己的输入法。而开发输入法最好的参考就是Android自带的Sample-SoftKeyboard,虽然这个例子仅包含英文和数字输入,但是它本身还算完整和清楚,对我们开始Android开发实战有很大帮助。
一、IMF简介
一个IMF结构中包含三个主要的部分:
input method manager:管理各部分的交互。它是一个客户端API,存在于各个应用程序的context中,用来沟通管理所有进程间交互的全局系统服务。
input method(IME):实现一个允许用户生成文本的独立交互模块。系统绑定一个当前的输入法。使其创建和生成,决定输入法何时隐藏或者显示它的UI。同一时间只能有一个IME运行。
client application:通过输入法管理器控制输入焦点和IME的状态。一次只能有一个客户端使用IME。
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吧。
2、InputMethodService
包括输入法内部逻辑,键盘布局,选词等,最终把选出的字符通过commitText提交出来。实现输入法的基础就是名为InputMethodService的类,比如你要实现一个谷歌输入法,就是要extends本类。我们接下来要学习的SoftKeyboard Sample也是extends本类。InputMethodService类的位置在:%SDK_ROOM%/docs/reference/android/inputmethodservice/InputMethodService.html
InputMethodService是InputMethod的一个完整实现,你可以再在其基础上扩展和定制。它的主要方法如下:
基本上输入法的定制,都是围绕在这个类来实现的,它主要提供的是一个基本的用户界面框架(包括输入视图,候选词视图和全屏模式),但是这些都是要实现者自己去定制的。这里的实现是让所有的元素都放置在了一个单一的由InputMethodService来管理的窗口中。它提供了很多的回调API,需要我们自己去实现。一些默认的设置包括:
两个非常重要的视图:
1. 软输入视图。是与用户交互的主要发生地:按键,画图或者其他的方式。通常的实现就是简单的用一个视图来处理所有的工作,并且在调用 onCreateInputView()的时候返回一个新的实例。通过调用系统的onEvaluateInputViewShow()来测试是否需要显示输入视图,它是系统根据当前的上下文环境来实现的。当输入法状态改变的时候,需要调用updateInputViewShown()来重新估计一下。
2. 候选词视图。当用户输入一些字符之后,输入法可能需要提供给用户一些可用的候选词的列表。这个视图的管理和输入视图不大一样,因为这个视图是非常的短暂的,它只是在有候选词的时候才会被显示。可以用setCandidatesViewShow()来设置是否需要显示这个视图。正是因为这个显示的频繁性,所以它一般不会被销毁,而且不会改变当前应用程序的视图。
最后,关于文本的产生,这是一个IME的最终目的。它通过InputConnection来链接IME和应用程序的:能够直接产生想要的按键信息,甚至直接在候选和提交的文本中编辑。当用户在不同的输入目标之间切换的时候,IME会不断的调用onFinishInput() 和 onStartInput()。在这两个函数中,需要反复做的就是复位状态,并且应对新的输入框的信息。
以上是一个输入法的最基本的介绍,下面将根据Sample中的SoftKeyboard来说明这些问题。
二、创建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。
之后就可以看到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这四个文件:
(二)LatinKeyboard.java
软键盘类,直接继承了Keyboard类,并定义一个xml格式的Keyboard的布局,来实现一个输入拉丁文的键盘。这里只是创建一个键盘对象,并不对具体的布局给出手段。
为了更好的理解LatinKeyboard类,这里简单介绍一下Keyboard类。Keyboard可以载入一个用来显示键盘布局的xml来初始化自己,并且可以保存这些键盘的键的属性。他有三个构造函数:
本文件源码前面完全继承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信息,这里有:
最后,它还定义了一个内部类——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. 先看它定义了那些重要变量:
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();我们分别解释如下:
在这里,distanceX是上次调用onscroll后滚动的X轴距离。假设这个view之前没有被滚动过,第一次滚动且坐标在显示区域内,sx=getScrollX()+distanceX,则view就scrollTo这个位置。如果sx超过了最大显示宽度,则scrollTo就滚想原先sx处,也就是不动。也就是说:系统滚动产生一个惯性的感觉,当你把view实际到了X坐标点,系统再给你加一个distanceX,这个distanceX不是两个动作之间的距离,应该是上一个滚动动作的停止点和本次滚动动作的停止点之间的距离,这个距离系统自己算,我们不用管,只要到了最大边界,view就不再滚动,或者说是原地滚动。
接下来:
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
以上两个blog同步更新。