OPhone 自定义输入法教程 

摘要:

在本教程中,将通过一个股票输入法实例来详细介绍如何在OPhone1.5中创建自定义输入法。
OPhone输入法介绍

得益于OPhone1.5的良好架构,在OPhone中创建输入法可不再是一件多么复杂的事情了。下面来看看OPhone中输入法的组成部分以及如何实现。
 

图一:OPhone系统默认输入法

上图是OPhone系统内置的输入法,可以看到一个输入法具有两个组成部分。第一是位于下方的输入软键盘(KeyboardView),用来输入文本和符号;第二是软键盘上面的输入候选区域(CandidateView),通过用户的输入提供一些可能的组合让用户选择。要触发输入法显示还需要一个输入目标,在上图的输入目标就是短消息内容文本,而短消息这个程序就是触发输入法的客户端程序。

软键盘的实现:在OPhone 1.5中软键盘是很容易实现的,通过android.inputmethodservice.Keyboard 类来创建软键盘,该类从XML文件中读取软键盘信息。有多少行,每行有多少按键,每个按键代表什么内容 等等。

候选区域的实现:对于中文输入法来说候选区域是一个特别重要的内容,拿拼音输入法来说用户输入拼音会出现多个候选词语,通过对候选词语的合理安排是一个输入法是否好用的重要评判标准之一。但是也有特殊情况下不需要候选区域的,例如输入数字或则密码。候选区域通过继承View实现。

在OPhone中输入法是一个服务(android.app.Service),用户通过点击输入目标来触发该服务,然后显示输入法,OPhone系统提供了一个Service实现android.inputmethodservice.InputMethodService,InputMethodService提供了一些接口方便实现输入法。下面就来看看每个部分如何实现,在下面的示例中通过一个股票输入键盘来讲解,经常炒股的人都知道在股票软件中有一些特殊组合输入,例如:600、000、A、C、F(在输入帐户的时候)。

软键盘实现

通过android.inputmethodservice.Keyboard只需要在XML文件中定义键盘布局就可以了,下面是股票软键盘的XML代码:

1. res\xml\stock.xml    
2. <?xml version="1.0" encoding="utf-8"?>
3. <Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
4. android:keyWidth="20%p"
5. android:horizontalGap="0px"
6. android:verticalGap="0px"
7. android:keyHeight="@dimen/key_height"
8. >
9.     <Row>   
10.         <Key android:codes="49" android:keyLabel="1"
11. android:keyEdgeFlags="left"/>
12.         <Key android:codes="50" android:keyLabel="2"/>
13.         <Key android:codes="51" android:keyLabel="3"/>
14.         <Key android:codes="52" android:keyLabel="4"/>
15.         <Key android:codes="53" android:keyLabel="5"
16. android:keyEdgeFlags="right"/>
17.     </Row>   
18.     <Row>   
19.         <Key android:codes="54" android:keyLabel="6"
20. android:keyEdgeFlags="left"/>
21.         <Key android:codes="55" android:keyLabel="7"/>
22.         <Key android:codes="56" android:keyLabel="8"/>
23.         <Key android:codes="57" android:keyLabel="9"/>
24.         <Key android:codes="48" android:keyLabel="0"
25. android:keyEdgeFlags="right"/>
26.     </Row>   
27.     <Row>   
28.         <Key android:codes="97" android:keyLabel="a"
29. android:keyEdgeFlags="left"/>
30.         <Key android:codes="99" android:keyLabel="c"/>
31.         <Key android:codes="102" android:keyLabel="f"/>
32.         <Key android:codes="46" android:keyLabel="."/>
33.         <Key android:codes="-5"
34. android:keyIcon="@drawable/sym_keyboard_delete"
35. android:keyEdgeFlags="right"
36. android:isRepeatable="true"/>
37.     </Row>   
38.     <Row  android:rowEdgeFlags="bottom">
39.         <Key android:codes="-3" android:keyWidth="20%p"
40. android:keyIcon="@drawable/sym_keyboard_done"
41. android:keyEdgeFlags="left" />
42.         <Key android:codes="-2" android:keyLabel="123" android:keyWidth="20%p" />
43.         <Key android:keyOutputText="600" android:keyLabel="600"
44. android:keyWidth="20%p" />
45.         <Key android:keyOutputText="000" android:keyLabel="000"
46. android:keyWidth="20%p" />
47.         <Key android:codes="10" android:keyWidth="20%p"
48. android:keyIcon="@drawable/sym_keyboard_return"
49. android:keyEdgeFlags="right"/>
50.     </Row>   
51. </Keyboard>
res\xml\stock.xml <?xml version="1.0" encoding="utf-8"?> <Keyboard xmlns:android="http://schemas.android.com/apk/res/android" android:keyWidth="20%p" android:horizontalGap="0px" android:verticalGap="0px" android:keyHeight="@dimen/key_height" > <Row> <Key android:codes="49" android:keyLabel="1" android:keyEdgeFlags="left"/> <Key android:codes="50" android:keyLabel="2"/> <Key android:codes="51" android:keyLabel="3"/> <Key android:codes="52" android:keyLabel="4"/> <Key android:codes="53" android:keyLabel="5" android:keyEdgeFlags="right"/> </Row> <Row> <Key android:codes="54" android:keyLabel="6" android:keyEdgeFlags="left"/> <Key android:codes="55" android:keyLabel="7"/> <Key android:codes="56" android:keyLabel="8"/> <Key android:codes="57" android:keyLabel="9"/> <Key android:codes="48" android:keyLabel="0" android:keyEdgeFlags="right"/> </Row> <Row> <Key android:codes="97" android:keyLabel="a" android:keyEdgeFlags="left"/> <Key android:codes="99" android:keyLabel="c"/> <Key android:codes="102" android:keyLabel="f"/> <Key android:codes="46" android:keyLabel="."/> <Key android:codes="-5" android:keyIcon="@drawable/sym_keyboard_delete" android:keyEdgeFlags="right" android:isRepeatable="true"/> </Row> <Row android:rowEdgeFlags="bottom"> <Key android:codes="-3" android:keyWidth="20%p" android:keyIcon="@drawable/sym_keyboard_done" android:keyEdgeFlags="left" /> <Key android:codes="-2" android:keyLabel="123" android:keyWidth="20%p" /> <Key android:keyOutputText="600" android:keyLabel="600" android:keyWidth="20%p" /> <Key android:keyOutputText="000" android:keyLabel="000" android:keyWidth="20%p" /> <Key android:codes="10" android:keyWidth="20%p" android:keyIcon="@drawable/sym_keyboard_return" android:keyEdgeFlags="right"/> </Row> </Keyboard>

上面的代码实现的键盘界面如下:
  

图二:股票键盘界面

在上面的键盘定义中,通过Keyboard说明是一个软键盘定义文件,Row元素说明这是一行按键的定义,Key元素说明这是一个按键的定义。Key元素通过一些属性来定义每个按键,下面是一些常用的属性介绍:

•  Codes:代表按键对应的输出值,可以为unicode值或则逗号(,)分割的多个值,也可以为一个字符串。在字符串中通过“\\”来转义特殊字符,例如 '\\n' 或则 '\\uxxxx' 。Codes通常用来定义该键的键码,例如上图中的数字按键1对应的为49;如果提供的是逗号分割的多个值则和普通手机输入键盘一样在多个值之间切换。 
•  keyLabel:代表按键显示的文本内容。 
•  keyIcon:代表按键显示的图标内容,如果指定了该值则在显示的时候显示为图片不显示文本。 
• keyWidth:代表按键的宽度,可以为精确值或则相对值,对于精确值支持多种单位,例如:像素,英寸 等;相对值为相对于基础取值的百分比,为以% 或则%p 结尾,其中%p表示相对于父容器。 
• keyHeight:代表按键的高度,取值同上。 
• horizontalGap:代表按键前的间隙(水平方向),取值同上。 
• isSticky:指定按键是否为sticky的。例如Shift大小写切换按键,具有两种状态,按下状态和正常状态,取值为true或则false。 
• isModifier:指定按键是否为功能键( modifier key ) ,例如 Alt 或则 Shift 。取值为true或则false。 
• keyOutputText:指定按键输出的文本内容,取值为字符串。 
• isRepeatable:指定按键是否是可重复的,如果长按该键可以触发重复按键事件则为true,否则为false。 
• keyEdgeFlags:指定按键的对齐指令,取值为left或则right。

在OPhone默认输入法中,如果统一页面有多个输入框,则软键盘中的enter键为变为下一个特殊按键,点击该按键可以导航到下一个输入框中,这样可以方便用户输入操作。要实现该功能可以通过自定义Keyboard来实现。
src\org\goodev\ime\StockKeyboard.java

1. public class StockKeyboard extends
2.    
3. private
4.        
5. public StockKeyboard(Context context, int
6. super(context, xmlLayoutResId);    
7.     }    
8.    
9. public StockKeyboard(Context context, int
10. int columns, int
11. super(context, layoutTemplateResId, characters, columns, horizontalPadding);    
12.     }    
13.        
14. @Override
15. protected Key createKeyFromXml(Resources res, Row parent, int x, int
16.             XmlResourceParser parser) {    
17. new
18. if (key.codes[0] == 10) {    
19.             mEnterKey = key;    
20.         }    
21. return
22.     }    
23.        
24. /** 
25.      * 根据输入状态,设置enter按键的显示内容。 
26.      */
27. void setImeOptions(Resources res, int
28. if (mEnterKey == null) {    
29. "StockKeyBoard: ", "enterkey == null");    
30. return;    
31.         }    
32.            
33. switch
34. case
35. null;    
36. null;    
37.                 mEnterKey.label = res.getText(R.string.label_go_key);    
38. break;    
39. case
40. null;    
41. null;    
42.                 mEnterKey.label = res.getText(R.string.label_next_key);    
43. break;    
44. case
45.                 mEnterKey.icon = res.getDrawable(    
46.                         R.drawable.sym_keyboard_search);    
47. null;    
48. break;    
49. case
50. null;    
51. null;    
52.                 mEnterKey.label = res.getText(R.string.label_send_key);    
53. break;    
54. default:    
55.                 mEnterKey.icon = res.getDrawable(    
56.                         R.drawable.sym_keyboard_return);    
57. null;    
58. break;    
59.         }    
60.     }    
61.    
62. }
public class StockKeyboard extends Keyboard { private Key mEnterKey; public StockKeyboard(Context context, int xmlLayoutResId) { super(context, xmlLayoutResId); } public StockKeyboard(Context context, int layoutTemplateResId, CharSequence characters, int columns, int horizontalPadding) { super(context, layoutTemplateResId, characters, columns, horizontalPadding); } @Override protected Key createKeyFromXml(Resources res, Row parent, int x, int y, XmlResourceParser parser) { Key key = new Key(res, parent, x, y, parser); if (key.codes[0] == 10) { mEnterKey = key; } return key; } /** * 根据输入状态,设置enter按键的显示内容。 */ void setImeOptions(Resources res, int options) { if (mEnterKey == null) { Log.d("StockKeyBoard: ", "enterkey == 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: mEnterKey.icon = res.getDrawable( R.drawable.sym_keyboard_return); mEnterKey.label = null; break; } } }

软键盘是放到KeyboardView中的,这里也自定义一个KeyboardView实现来处理特殊按键事件:
src\org\goodev\ime\StockKeyboardView.java

1. public class StockKeyboardView extends
2. public static final int KEYCODE_OPTIONS = -100;    
3. public
4. super(context, attrs);    
5.     }    
6. public StockKeyboardView(Context context, AttributeSet attrs, int
7. super(context, attrs, defStyle);    
8.     }    
9. /** 
10.      * 覆写这个方法,当用户长按CANCEL键的时候 抛出事件,可以用来现实现实输入法选项的操作 
11.      */
12. @Override
13. protected boolean
14. if (key.codes[0] == Keyboard.KEYCODE_CANCEL) {    
15. null);    
16. return true;    
17. else
18. return super.onLongPress(key);    
19.         }    
20.     }    
21. }
public class StockKeyboardView extends KeyboardView { public static final int KEYCODE_OPTIONS = -100; public StockKeyboardView(Context context, AttributeSet attrs) { super(context, attrs); } public StockKeyboardView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } /** * 覆写这个方法,当用户长按CANCEL键的时候 抛出事件,可以用来现实现实输入法选项的操作 */ @Override protected boolean onLongPress(Key key) { if (key.codes[0] == Keyboard.KEYCODE_CANCEL) { getOnKeyboardActionListener().onKey(KEYCODE_OPTIONS, null); return true; } else { return super.onLongPress(key); } } }

候选区域实现

示例中的候选区域只是扩展View的简单实现,具体实现可以参考附件中的src\org\goodev\ime\CandidateView.java代码。

定义输入法服务

通过扩展android.inputmethodservice.InputMethodService可以很容易的实现一个输入法服务,InputMethodService提供了一些系统回调函数,可以安装需要来实现。在具体实现之前,先来了解下InputMethodService的生命周期。

图三:InputMethodService生命周期

由上图可以看到,在用户触发输入法显示的时候(点击输入框),InputMethodService启动,然后调用onCreate() 函数,该函数在输入法第一次启动的时候调用,适合用来做一些初始化的设置,切忌不要在代码中直接调用该函数;然后调用onCreateInputView() 函数,在该函数中创建KeyboardView并返回;然后调用onCreateCandidatesView()函数,在该函数中创建候选区实现并返回;然后调用onStartInputView(EditorInfo attribute, boolean restarting)函数来开始输入内容,输入结束后调用onFinishInput()函数来结束当前的输入,如果移动到下一个输入框则重复调用onStartInputView和onFinishInput函数;在输入法关闭的时候调用onDestroy() 函数。
下面就来看看在src\org\goodev\ime\StockIME.java中各个回调函数的实现:

1. public void
2. super.onCreate();    
3.     mWordSeparators = getResources().getString(R.string.word_separators);
public void onCreate() { super.onCreate(); mWordSeparators = getResources().getString(R.string.word_separators); }

在onCreate函数中初始化文本分隔符,如果输入中遇到分隔符则提交当前输入的内容。

1. public
2.         mInputView = (KeyboardView) getLayoutInflater().inflate(    
3. null);    
4. this);    
5.         mInputView.setKeyboard(mQwertyKeyboard);    
6. return
7. }
public View onCreateInputView() { mInputView = (KeyboardView) getLayoutInflater().inflate( R.layout.input, null); mInputView.setOnKeyboardActionListener(this); mInputView.setKeyboard(mQwertyKeyboard); return mInputView; }

在onCreateInputView中创建KeyboardView,并设置Keyboard监听器和当前Keyboard。R.layout.input 引用的是一个自定义的KeyboardView资源文件,内容如下:
res\layout\input.xml

1. <org.goodev.ime.StockKeyboardView    
2. "http://schemas.android.com/apk/res/android"
3. "@+id/keyboard"
4. "true"
5. "fill_parent"
6. "wrap_content"
7.         />    
8.    
9. public
10. new CandidateView(this);    
11. this);    
12. return
13.     }
<org.goodev.ime.StockKeyboardView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/keyboard" android:layout_alignParentBottom="true" android:layout_width="fill_parent" android:layout_height="wrap_content" /> public View onCreateCandidatesView() { mCandidateView = new CandidateView(this); mCandidateView.setService(this); return mCandidateView; }

在onCreateCandidatesView中创建CandidateView并返回。

    1. public void onStartInputView(EditorInfo attribute, boolean
    2. super.onStartInputView(attribute, restarting);    
    3. // Apply the selected keyboard to the input view. 
    4.       mInputView.setKeyboard(mCurKeyboard);    
    5.       mInputView.closing();
    public void onStartInputView(EditorInfo attribute, boolean restarting) { super.onStartInputView(attribute, restarting); // Apply the selected keyboard to the input view. mInputView.setKeyboard(mCurKeyboard); mInputView.closing(); }

    在onStartInputView中设置当前的软键盘。

    1. public void
    2. super.onFinishInput();    
    3. // Clear current composing text and candidates. 
    4. 0);    
    5.     updateCandidates();    
    6. // 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. 
    7. false);    
    8.     mCurKeyboard = mQwertyKeyboard;    
    9. if (mInputView != null) {    
    10.         mInputView.closing();    
    11.     }
    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(); } }

    在onFinishInput中隐藏候选区域。
    另外还有一些回调接口上图没有显示,例如onInitializeInterface()和onStartInput(EditorInfo attribute, boolean restarting)等。另外示例中还演示了如何通过长按取消按键来打开输入法设置选项窗口,如下图:
     具体详细实现逻辑请参考附件中的src\org\goodev\ime\StockIME.java文件。

    定义输入法服务

    最后只需要在AndroidManifest.xml中定义Service就可以了:

    1. <?xml version="1.0" encoding="utf-8"?>
    2. <manifest   
    3. xmlns:android="http://schemas.android.com/apk/res/android"
    4. package="org.goodev.ime"
    5. android:versionCode="1"
    6. android:versionName="1.0">
    7.     <application android:icon="@drawable/icon"
    8. android:label="@string/app_name">
    9.         <service android:name="StockIME"
    10. android:permission="android.permission.BIND_INPUT_METHOD">
    11.             <intent-filter>   
    12.                 <action android:name="android.view.InputMethod"/>
    13.             </intent-filter>   
    14.             <meta-data android:name="android.view.im"
    15. android:resource="@xml/method"/>
    16.         </service>   
    17.     </application>   
    18.     <uses-sdk android:minSdkVersion="3" />
    19. </manifest>
    <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.goodev.ime" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <service android:name="StockIME" 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> <uses-sdk android:minSdkVersion="3" /> </manifest>

    输入法在OPhone系统中是一个服务(Service),通过AndroidManifest.xml中的service定义,该服务具有android.permission.BIND_INPUT_METHOD权限,表明这是一个输入法服务。在intent-filter中使用android.view.InputMethod action来定义,最后通过name为android.view.im的meta-data来描述该输入法的一些属性,meta-data引用的是一个XML文件,该文件是输入法的配置文件,用来配置一些信息,例如是否为默认输入法,是否具有配置Activity来配置输入法的一些选项,如果指定了配置Activity则在系统设置界面中的输入法设置中可以启动该Activity来设置输入法的配置项。下面是一个示例配置文件:

    1. <?xml version="1.0" encoding="utf-8"?>
    2. <!-- The attributes in this XML file provide configuration information -->
    3. <!-- for the Search Manager. -->
    4.    
    5. <input-method   
    6. xmlns:android="http://schemas.android.com/apk/res/android"
    7. android:settingsActivity="com.android.inputmethod.latin.LatinIMESettings"
    8. android:isDefault="@bool/im_is_default"/>
    <?xml version="1.0" encoding="utf-8"?> <!-- The attributes in this XML file provide configuration information --> <!-- for the Search Manager. --> <input-method xmlns:android="http://schemas.android.com/apk/res/android" android:settingsActivity="com.android.inputmethod.latin.LatinIMESettings" android:isDefault="@bool/im_is_default"/>

    小提示:在输入框上长按会弹出快捷菜单“输入方法”,可以从中切换输入法。

    总结:

    从输入法创建过程可以看出,OPhone1.5提供的良好的输入法框架可以很方便的开发第三方输入法,开发者只需要关注于输入法的业务逻辑实现,界面部分交给OPhone来做就可以了,这样可以极大的节省开发人员的精力和开发成本。