输入法
IMF(Input Method Frameworks)是Android输入法的Framework框架,其中最主要的是InputMethodService,他继承于AbstractInputMethodService。
它主要由以下几个组件构成,完成输入法的相关UI,和文字的输出。
1. Soft Input View
这是软键盘的Input Area,主要完成touch screen下和用户的交互输入。onCreateInputView() 被调用来进行soft inputview的实例化;onEvaluateInputViewShown() 决定是否显示soft inputview;当状态改变的时候,调用updateInputViewShown() 来重新决策是否显示soft inputview。
2. Candidates View
Candidates View也是输入法中一个相当重要的组件。当用户输入字符的时候,显示相关的列表。停止输入的时候,有会自动消失。onCreateCandidatesView() 来实例化自己的IME。和soft inputview不同的是Candidates View对整个UI布局不会产生影响。setCandidatesViewShown(boolean) 用来设置是否显示Candidates View。
3. 输出字符
字符的输出是InputMethodService最核心的功能,IME通过 InputConnection 从IMF来获得字符输出。并且通过不同的editor类型来获取相应的支持。通过
onFinishInput()
和onStartInput(EditorInfo, boolean) 方法来进行输入目标的切换。
另外,onInitializeInterface() 用于InputMethodService在执行的过程中配置的改变;
onBindInput() 切换一个新的输入通道;
onStartInput(EditorInfo, boolean) 处理一个新的输入。
InputConnection是IMF里面一个重要的接口,他是实现BaseInputConnection和InputConnectionWrapper 上层的接口。主要用于应用程序和InputMethod 之间通信的通道,包括实现读取关标周围的输入,向文本框中输入文本以及给应用程序发送各种按键事件。
- InputConnection ic = getCurrentInputConnection(); //获取InputConnection接口
- if (ic != null){
- ic.beginBatchEdit(); //开始输入编辑操作
- if (isShifted()) {
- text = text.toString().toUpperCase();
- }
- ic.commitText(text, 1); //将text字符输入文本框,并且将关标移至字符做插入态
- ic.endBatchEdit(); //完成编辑输入
- }
InputConnection ic = getCurrentInputConnection(); //获取InputConnection接口
if (ic != null){
ic.beginBatchEdit(); //开始输入编辑操作
if (isShifted()) {
text = text.toString().toUpperCase();
}
ic.commitText(text, 1); //将text字符输入文本框,并且将关标移至字符做插入态
ic.endBatchEdit(); //完成编辑输入
}
接口InputMethod是上节说到的AbstractInputMethodService和InputMethodService的上层接口,它可以产生各种按键事件和各种字符文本。
所有的IME客户端都要绑定BIND_INPUT_METHOD ,这是IMF出于对安全的角度的考量,对使用InputMethodService的一个所有客户端的强制要求。否则系统会拒绝此客户端使用InputMethod。
- <service android:name="IME"
- android:label="@string/SoftkeyIME"
- android:permission="android.permission.BIND_INPUT_METHOD">
<service android:name="IME" android:label="@string/SoftkeyIME" android:permission="android.permission.BIND_INPUT_METHOD">
在这个接口中有
bindInput (InputBinding binding) 绑定一个一个应用至输入法;
createSession (InputMethod.SessionCallback callback) 创建一个新的InputMethodSession 用于客户端与输入法的交互;
startInput (InputConnection inputConnection, EditorInfo info) 输入法准备就绪开始接受各种事件并且将输入的文本返回给应用程序;
unbindInput () 取消应用程序和输入法的绑定;
showSoftInput (int flags, ResultReceiver resultReceiver) 和hideSoftInput (int flags, ResultReceiver resultReceiver) 顾名思义是显示和隐藏软键盘输入。
InputMethodSession是一个可以安全暴露给应用程序使用的接口,他需要由InputMethodService和 InputMethodSessionImpl 实现。
以下是一段在Framework中取到的代码,可以比较全面的反应这个接口的使用:
- final InputMethodSession mInputMethodSession;
final InputMethodSession mInputMethodSession;
- public void executeMessage(Message msg) {
- switch (msg.what) {
- case DO_FINISH_INPUT:
- mInputMethodSession.finishInput(); //应用程序停止接收字符
- return;
- case DO_DISPLAY_COMPLETIONS:
- mInputMethodSession.displayCompletions((CompletionInfo[])msg.obj); //输入法自动完成功能
- return;
- case DO_UPDATE_EXTRACTED_TEXT:
- mInputMethodSession.updateExtractedText(msg.arg1,
- (ExtractedText)msg.obj);
- return;
- case DO_DISPATCH_KEY_EVENT: {
- HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj;
- mInputMethodSession.dispatchKeyEvent(msg.arg1,
- (KeyEvent)args.arg1,
- new InputMethodEventCallbackWrapper(
- (IInputMethodCallback)args.arg2)); //处理按键
- mCaller.recycleArgs(args);
- return;
- }
- case DO_DISPATCH_TRACKBALL_EVENT: {
- HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj;
- mInputMethodSession.dispatchTrackballEvent(msg.arg1,
- (MotionEvent)args.arg1,
- new InputMethodEventCallbackWrapper(
- (IInputMethodCallback)args.arg2));
- mCaller.recycleArgs(args);
- return;
- }
- case DO_UPDATE_SELECTION: {
- HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj;
- mInputMethodSession.updateSelection(args.argi1, args.argi2,
- args.argi3, args.argi4, args.argi5, args.argi6); //更新选取的字符
- mCaller.recycleArgs(args);
- return;
- }
- case DO_UPDATE_CURSOR: {
- mInputMethodSession.updateCursor((Rect)msg.obj); //更新关标
- return;
- }
- case DO_APP_PRIVATE_COMMAND: {
- HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj;
- mInputMethodSession.appPrivateCommand((String)args.arg1,
- (Bundle)args.arg2); //处理应用程序发给输入法的命令
- mCaller.recycleArgs(args);
- return;
- }
- case DO_TOGGLE_SOFT_INPUT: {
- mInputMethodSession.toggleSoftInput(msg.arg1, msg.arg2); //切换软键盘
- return;
- }
- }
- Log.w(TAG, "Unhandled message code: " + msg.what);
- }
我们知道当一个编辑框获得焦点的时候,将会create一个新的IME客户端,那么IMF框架是怎样处理这个事件的呢?
首先调用EditText的构造函数EditTex->TextView->setText()
- InputMethodManager imm = InputMethodManager.peekInstance();
- if (imm != null) imm.restartInput(this);
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) imm.restartInput(this);
->startInputInner
- void startInputInner() {
- final View view;
- synchronized (mH) {
- view = mServedView;
- // Make sure we have a window token for the served view.
- if (DEBUG) Log.v(TAG, "Starting input: view=" + view);
- if (view == null) {
- if (DEBUG) Log.v(TAG, "ABORT input: no served view!");
- return;
- }
- }
- // Now we need to get an input connection from the served view.
- // This is complicated in a couple ways: we can't be holding our lock
- // when calling out to the view, and we need to make sure we call into
- // the view on the same thread that is driving its view hierarchy.
- Handler vh = view.getHandler();
- if (vh == null) {
- // If the view doesn't have a handler, something has changed out
- // from under us, so just bail.
- if (DEBUG) Log.v(TAG, "ABORT input: no handler for view!");
- return;
- }
- if (vh.getLooper() != Looper.myLooper()) {
- // The view is running on a different thread than our own, so
- // we need to reschedule our work for over there.
- if (DEBUG) Log.v(TAG, "Starting input: reschedule to view thread");
- vh.post(new Runnable() {
- public void run() {
- startInputInner();
- }
- });
- return;
- }
- // Okay we are now ready to call into the served view and have it
- // do its stuff.
- // Life is good: let's hook everything up!
- EditorInfo tba = new EditorInfo();
- tba.packageName = view.getContext().getPackageName();
- tba.fieldId = view.getId();
- InputConnection ic = view.onCreateInputConnection(tba); //create 新的InputConnection
- if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);
- synchronized (mH) {
- // Now that we are locked again, validate that our state hasn't
- // changed.
- if (mServedView != view || !mServedConnecting) {
- // Something else happened, so abort.
- if (DEBUG) Log.v(TAG,
- "Starting input: finished by someone else (view="
- + mServedView + " conn=" + mServedConnecting + ")");
- return;
- }
- // If we already have a text box, then this view is already
- // connected so we want to restart it.
- final boolean initial = mCurrentTextBoxAttribute == null;
- // Hook 'em up and let 'er rip.
- mCurrentTextBoxAttribute = tba;
- mServedConnecting = false;
- mServedInputConnection = ic;
- IInputContext servedContext;
- if (ic != null) {
- mCursorSelStart = tba.initialSelStart;
- mCursorSelEnd = tba.initialSelEnd;
- mCursorCandStart = -1;
- mCursorCandEnd = -1;
- mCursorRect.setEmpty();
- servedContext = new ControlledInputConnectionWrapper(vh.getLooper(), ic);
- } else {
- servedContext = null;
- }
- try {
- if (DEBUG) Log.v(TAG, "START INPUT: " + view + " ic="
- + ic + " tba=" + tba + " initial=" + initial);
- InputBindResult res = mService.startInput(mClient,
- servedContext, tba, initial, mCurMethod == null); //启动IMEservice
- if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
- if (res != null) {
- if (res.id != null) {
- mBindSequence = res.sequence;
- mCurMethod = res.method;
- } else {
- // This means there is no input method available.
- if (DEBUG) Log.v(TAG, "ABORT input: no input method!");
- return;
- }
- }
- if (mCurMethod != null && mCompletions != null) {
- try {
- mCurMethod.displayCompletions(mCompletions);
- } catch (RemoteException e) {
- }
- }
- } catch (RemoteException e) {
- Log.w(TAG, "IME died: " + mCurId, e);
- }
- }
void startInputInner() {
final View view;
synchronized (mH) {
view = mServedView;
// Make sure we have a window token for the served view.
if (DEBUG) Log.v(TAG, "Starting input: view=" + view);
if (view == null) {
if (DEBUG) Log.v(TAG, "ABORT input: no served view!");
return;
}
}
// Now we need to get an input connection from the served view.
// This is complicated in a couple ways: we can't be holding our lock
// when calling out to the view, and we need to make sure we call into
// the view on the same thread that is driving its view hierarchy.
Handler vh = view.getHandler();
if (vh == null) {
// If the view doesn't have a handler, something has changed out
// from under us, so just bail.
if (DEBUG) Log.v(TAG, "ABORT input: no handler for view!");
return;
}
if (vh.getLooper() != Looper.myLooper()) {
// The view is running on a different thread than our own, so
// we need to reschedule our work for over there.
if (DEBUG) Log.v(TAG, "Starting input: reschedule to view thread");
vh.post(new Runnable() {
public void run() {
startInputInner();
}
});
return;
}
// Okay we are now ready to call into the served view and have it
// do its stuff.
// Life is good: let's hook everything up!
EditorInfo tba = new EditorInfo();
tba.packageName = view.getContext().getPackageName();
tba.fieldId = view.getId();
InputConnection ic = view.onCreateInputConnection(tba); //create 新的InputConnection
if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);
synchronized (mH) {
// Now that we are locked again, validate that our state hasn't
// changed.
if (mServedView != view || !mServedConnecting) {
// Something else happened, so abort.
if (DEBUG) Log.v(TAG,
"Starting input: finished by someone else (view="
+ mServedView + " conn=" + mServedConnecting + ")");
return;
}
// If we already have a text box, then this view is already
// connected so we want to restart it.
final boolean initial = mCurrentTextBoxAttribute == null;
// Hook 'em up and let 'er rip.
mCurrentTextBoxAttribute = tba;
mServedConnecting = false;
mServedInputConnection = ic;
IInputContext servedContext;
if (ic != null) {
mCursorSelStart = tba.initialSelStart;
mCursorSelEnd = tba.initialSelEnd;
mCursorCandStart = -1;
mCursorCandEnd = -1;
mCursorRect.setEmpty();
servedContext = new ControlledInputConnectionWrapper(vh.getLooper(), ic);
} else {
servedContext = null;
}
try {
if (DEBUG) Log.v(TAG, "START INPUT: " + view + " ic="
+ ic + " tba=" + tba + " initial=" + initial);
InputBindResult res = mService.startInput(mClient,
servedContext, tba, initial, mCurMethod == null); //启动IMEservice
if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
if (res != null) {
if (res.id != null) {
mBindSequence = res.sequence;
mCurMethod = res.method;
} else {
// This means there is no input method available.
if (DEBUG) Log.v(TAG, "ABORT input: no input method!");
return;
}
}
if (mCurMethod != null && mCompletions != null) {
try {
mCurMethod.displayCompletions(mCompletions);
} catch (RemoteException e) {
}
}
} catch (RemoteException e) {
Log.w(TAG, "IME died: " + mCurId, e);
}
}
那么到了IME部分的流程又是如何的呢?
首先收到configure change的event调用onConfigurationChanged->inputmethodservice.onConfigurationChanged(conf)
- @Override public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- boolean visible = mWindowVisible;
- int showFlags = mShowInputFlags;
- boolean showingInput = mShowInputRequested;
- CompletionInfo[] completions = mCurCompletions;
- initViews();
- mInputViewStarted = false;
- mCandidatesViewStarted = false;
- if (mInputStarted) {
- doStartInput(getCurrentInputConnection(),
- getCurrentInputEditorInfo(), true); //调用startinput,创建IME的input
- }
- if (visible) {
- if (showingInput) {
- // If we were last showing the soft keyboard, try to do so again.
- if (onShowInputRequested(showFlags, true)) {
- showWindow(true);
- if (completions != null) {
- mCurCompletions = completions;
- onDisplayCompletions(completions);
- }
- } else {
- hideWindow();
- }
- } else if (mCandidatesVisibility == View.VISIBLE) {
- // If the candidates are currently visible, make sure the
- // window is shown for them.
- showWindow(false);
- } else {
- // Otherwise hide the window.
- hideWindow();
- }
- }
- }
@Override public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
boolean visible = mWindowVisible;
int showFlags = mShowInputFlags;
boolean showingInput = mShowInputRequested;
CompletionInfo[] completions = mCurCompletions;
initViews();
mInputViewStarted = false;
mCandidatesViewStarted = false;
if (mInputStarted) {
doStartInput(getCurrentInputConnection(),
getCurrentInputEditorInfo(), true); //调用startinput,创建IME的input
}
if (visible) {
if (showingInput) {
// If we were last showing the soft keyboard, try to do so again.
if (onShowInputRequested(showFlags, true)) {
showWindow(true);
if (completions != null) {
mCurCompletions = completions;
onDisplayCompletions(completions);
}
} else {
hideWindow();
}
} else if (mCandidatesVisibility == View.VISIBLE) {
// If the candidates are currently visible, make sure the
// window is shown for them.
showWindow(false);
} else {
// Otherwise hide the window.
hideWindow();
}
}
}
doStartInput->initialize->onInitializeInterface->onStartInput->onStartInputView.
onStartInputView中会handle各种IME的event并进一步调用IME自身的onCreate函数,做进一步的初始化以及receiver的rigister。