Android输入法框架系统(上)
来源:互联网 发布:淘宝怎么关闭私人定制 编辑:程序博客网 时间:2024/04/28 01:01
输入法,就是用来输入字符(包括英文,俄文,中文)的工具。输入法你可以看成是一种字符发生器,它将输入数据触摸事件或者按键事件转化为其他更丰富的字符。在PC时代,输入法的原始输入来自实体键盘,鼠标,然后输入法将这些事件对应的ASCII码转换为俄文,中文,当然如果是英文是不需要转换,直接发送即可。而在Android系统里,由于输入法dialog永远没法成为焦点window,所以输入法永远没法获取到按键事件,也就是说输入法的输入数据只能来自触摸事件,输入法显示出键盘(大家称之为软键盘),用户点击键盘UI, 然后输入法将触摸事件所在位置的字符当做原始字符输入,最后组装成更为丰富的字符(多个字符组成拼音,然后转化为中文),然后就是发送到对应的程序。
Android系统中,输入法可以是可以安装的,也就是说系统可以有多个输入法((sougou输入法,百度输入法),但是只有一个是激活的,当然用户可以切换输入法。同时,输入法是以service的方式运行的,输入法同一时间只能服务一个程序,只有最顶层的可见的程序才能接收到输入法的输入数据。
输入法系统的整个框架如下:
InputMethodManagerService(下文也称IMMS)负责管理系统的所有输入法,包括输入法service(InputMethodService简称IMS)加载及切换。程序获得焦点时,就会通过InputMethodManager向InputMethodManagerService通知自己获得焦点并请求绑定自己到当前输入法上。同时,当程序的某个需要输入法的view比如EditorView获得焦点时就会通过InputMethodManager向InputMethodManagerService请求显示输入法,而这时InputMethodManagerService收到请求后,会将请求的EditText的数据通信接口发送给当前输入法,并请求显输入法。输入法收到请求后,就显示自己的UI dialog,同时保存目标view的数据结构,当用户实现输入后,直接通过view的数据通信接口将字符传递到对应的View。接下来就来分析这些过程。
InputMethodManager创建
每个程序有一个InputMethodManager实例,这个是程序和InputMethodManagerService通信的接口,该实例在ViewRootImpl初始化的时候创建。
public ViewRootImpl(Context context, Display display) { mContext = context; mWindowSession = WindowManagerGlobal.getWindowSession(); } public static IWindowSession getWindowSession() { synchronized (WindowManagerGlobal.class) { if (sWindowSession == null) { try { //这个进程的InputMethodManager实例就生成了 InputMethodManager imm = InputMethodManager.getInstance(); IWindowManager windowManager = getWindowManagerService(); } catch (RemoteException e) { Log.e(TAG, "Failed to open window session", e); } } return sWindowSession; } } public static InputMethodManager getInstance() { synchronized (InputMethodManager.class) { if (sInstance == null) { // InputMethodManager其实就是一个Binder service的proxy IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE); IInputMethodManager service = IInputMethodManager.Stub.asInterface(b); sInstance = new InputMethodManager(service, Looper.getMainLooper()); } return sInstance; } }
程序的Window获得焦点
程序的window获得焦点的时序图如下
系统WindowManagerService更新焦点window
哪个程序获得焦点是由系统决定的,是由WindowManagerService决定的,当系统的window状态发生变化时(比如window新增,删除)就会调用函数updateFocusedWindowLocked来更新焦点window。
private boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) { //计算焦点window WindowState newFocus = computeFocusedWindowLocked(); if (mCurrentFocus != newFocus) { //焦点window发生变化,post一个message来通知程序焦点发生变化了 mH.removeMessages(H.REPORT_FOCUS_CHANGE); mH.sendEmptyMessage(H.REPORT_FOCUS_CHANGE); return true; } return false; } private WindowState computeFocusedWindowLocked() { if (mAnimator.mUniverseBackground != null && mAnimator.mUniverseBackground.mWin.canReceiveKeys()) { return mAnimator.mUniverseBackground.mWin; } final int displayCount = mDisplayContents.size(); for (int i = 0; i < displayCount; i++) { final DisplayContent displayContent = mDisplayContents.valueAt(i); WindowState win = findFocusedWindowLocked(displayContent); if (win != null) { return win; } } return null; } //该函数就是找出最top的可以接收按键事件的window,这个window就获得焦点 private WindowState findFocusedWindowLocked(DisplayContent displayContent) { final WindowList windows = displayContent.getWindowList(); for (int i = windows.size() - 1; i >= 0; i--) { final WindowState win = windows.get(i); //是否为activity的window AppWindowToken wtoken = win.mAppToken; //重要函数,window是否可以获取焦点 if (!win.canReceiveKeys()) { continue; } // mFocusedApp是最top的activity ,下面逻辑是为了确保焦点window的app //必须是焦点程序之上,所以这个逻辑其实并没有多大作用,只是为了检测出 //错误 if (wtoken != null && win.mAttrs.type != TYPE_APPLICATION_STARTING && mFocusedApp != null) { ArrayList<Task> tasks = displayContent.getTasks(); for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) { AppTokenList tokens = tasks.get(taskNdx).mAppTokens; int tokenNdx = tokens.size() - 1; for ( ; tokenNdx >= 0; --tokenNdx) { final AppWindowToken token = tokens.get(tokenNdx); if (wtoken == token) { break; } if (mFocusedApp == token) { return null; } } } } return win; } return null; } public final boolean canReceiveKeys() { return isVisibleOrAdding() && (mViewVisibility == View.VISIBLE) && ((mAttrs.flags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) == 0); } //由于输入法的window带有FLAG_NOT_FOCUSABLE, 从上可见其不可能是焦点window //接下来系统开始通知程序端哪个window获得了焦点。 final class H extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case REPORT_FOCUS_CHANGE: { WindowState lastFocus; WindowState newFocus; synchronized(mWindowMap) { lastFocus = mLastFocus; newFocus = mCurrentFocus; if (lastFocus == newFocus) { // Focus is not changing, so nothing to do. return; } mLastFocus = newFocus; } if (newFocus != null) { //通知新的焦点程序其获得了焦点 newFocus.reportFocusChangedSerialized(true, mInTouchMode); notifyFocusChanged(); } if (lastFocus != null) { //通知老的焦点程序其获得了焦点 lastFocus.reportFocusChangedSerialized(false, mInTouchMode); } } break; } public void reportFocusChangedSerialized(boolean focused, boolean inTouchMode) { try { //这个就是通过Binder告知client其获得或失去了焦点 mClient.windowFocusChanged(focused, inTouchMode); } catch (RemoteException e) { } }
上面的mClient.windowFocusChanged会调回到ViewRootImpl中的W实例:
程序获得焦点改变事件
//ViewRootImpl.java static class W extends IWindow.Stub { @Override public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) { final ViewRootImpl viewAncestor = mViewAncestor.get(); if (viewAncestor != null) { viewAncestor.windowFocusChanged(hasFocus, inTouchMode); } } } public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) { Message msg = Message.obtain(); msg.what = MSG_WINDOW_FOCUS_CHANGED; msg.arg1 = hasFocus ? 1 : 0; msg.arg2 = inTouchMode ? 1 : 0; mHandler.sendMessage(msg); } //程序获得焦点会通过调用mView.dispatchWindowFocusChanged和 //imm.onWindowFocus来通知IMMS焦点信息发生改变,需要更新输入法了 final class ViewRootHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_WINDOW_FOCUS_CHANGED: { if (mAdded) { boolean hasWindowFocus = msg.arg1 != 0; mAttachInfo.mHasWindowFocus = hasWindowFocus; mLastWasImTarget = WindowManager.LayoutParams .mayUseInputMethod(mWindowAttributes.flags); InputMethodManager imm = InputMethodManager.peekInstance(); if (mView != null) { //调用根view的dispatchWindowFocusChanged函数通知view //程序获得焦点 mView.dispatchWindowFocusChanged(hasWindowFocus); mAttachInfo.mTreeObserver.dispatchOnWindowFocusChange(hasWindowFocus); } if (hasWindowFocus) { if (imm != null && mLastWasImTarget && !isInLocalFocusMode()) { //通知imm该window获得焦点 imm.onWindowFocus(mView, mView.findFocus(), mWindowAttributes.softInputMode, !mHasHadWindowFocus, mWindowAttributes.flags); } } } } break; } //上面的根view就是DecorView,它只是调用父类ViewGroup //的dispatchWindowFocusChanged //ViewGroup.java @Override public void dispatchWindowFocusChanged(boolean hasFocus) { super.dispatchWindowFocusChanged(hasFocus); final int count = mChildrenCount; final View[] children = mChildren; //让每个子view处理window焦点改变时间 //但是只有获得焦点的view才会处理这个时间 for (int i = 0; i < count; i++) { children[i].dispatchWindowFocusChanged(hasFocus); } } //View.java public void onWindowFocusChanged(boolean hasWindowFocus) { InputMethodManager imm = InputMethodManager.peekInstance(); if (!hasWindowFocus) { } else if (imm != null && (mPrivateFlags & PFLAG_FOCUSED) != 0) { //获得焦点的view通过 InputMethodManager向service通知自己获得焦点 imm.focusIn(this); } }
焦点View向IMMS请求绑定输入法
焦点view请求绑定输入法是通过调用InputMethodManager. focusIn实现的
public void focusIn(View view) { synchronized (mH) { focusInLocked(view); } } void focusInLocked(View view) { //保存焦点view变量 mNextServedView = view; scheduleCheckFocusLocked(view); } static void scheduleCheckFocusLocked(View view) { ViewRootImpl viewRootImpl = view.getViewRootImpl(); if (viewRootImpl != null) { viewRootImpl.dispatchCheckFocus(); } } public void dispatchCheckFocus() { if (!mHandler.hasMessages(MSG_CHECK_FOCUS)) { // This will result in a call to checkFocus() below. mHandler.sendEmptyMessage(MSG_CHECK_FOCUS); } } case MSG_CHECK_FOCUS: { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) { imm.checkFocus(); } } break; public void checkFocus() { if (checkFocusNoStartInput(false, true)) { startInputInner(null, 0, 0, 0); } } boolean startInputInner(IBinder windowGainingFocus, int controlFlags, int softInputMode, int windowFlags) { final View view; synchronized (mH) { //获得上面的焦点view view = mServedView; } EditorInfo tba = new EditorInfo(); tba.packageName = view.getContext().getPackageName(); tba.fieldId = view.getId(); //创建数据通信连接接口,这个会传送到InputMethodService //InputMethodService后面就通过这个connection将输入法的字符传递给该view InputConnection ic = view.onCreateInputConnection(tba); synchronized (mH) { mServedInputConnection = ic; ControlledInputConnectionWrapper servedContext; if (ic != null) { mCursorSelStart = tba.initialSelStart; mCursorSelEnd = tba.initialSelEnd; mCursorCandStart = -1; mCursorCandEnd = -1; mCursorRect.setEmpty(); //将InputConnection封装为binder对象,这个是真正可以实现跨进程通 //信的封装类 servedContext = new ControlledInputConnectionWrapper(vh.getLooper(), ic, this); } mServedInputConnectionWrapper = servedContext; try { InputBindResult res; if (windowGainingFocus != null) { //focusIn这个不会走到这条分支 res = mService.windowGainedFocus(mClient, windowGainingFocus, controlFlags, softInputMode, windowFlags, tba, servedContext); } else { //通知InputMethodManagerService,该程序的view获得焦点,IMMS //就会将这个view和输入法绑定 res = mService.startInput(mClient, servedContext, tba, controlFlags); } if (res != null) { if (res.id != null) { setInputChannelLocked(res.channel); mBindSequence = res.sequence; //获得了输入法的通信接口 mCurMethod = res.method; mCurId = res.id; } } } } return true; }
IMMS处理view绑定输入法事件
为了讲解整个绑定过程,我们假设此时输入法service还没启动,这个情况下的输入法绑定是最长的,整个过程经历过如下过程:
1) 启动输入法service
2) 绑定输入法window的token
3) 请求输入法为焦点程序创建一个连接会话-
4) 将输入法的接口传递回程序client端
5) 绑定输入法和焦点view
1-4是和程序相关的,而5是和view相关的。所以你可以说1~4是用来绑定程序window和输入法,而5是用来绑定程序view和输入法。
输入法还没启动时,弹出输入法会经过1~5,输入法已经启动,但是焦点window发生变化时会经历3~5,焦点window没有变化,只是改变了焦点view,则只会经历5。整个流程如下:
启动输入法service
@Override public InputBindResult startInput(IInputMethodClient client, IInputContext inputContext, EditorInfo attribute, int controlFlags) { synchronized (mMethodMap) { final long ident = Binder.clearCallingIdentity(); try { return startInputLocked(client, inputContext, attribute, controlFlags); } } } InputBindResult startInputLocked(IInputMethodClient client, IInputContext inputContext, EditorInfo attribute, int controlFlags) { //程序在service端对应的数据结构 ClientState cs = mClients.get(client.asBinder()); return startInputUncheckedLocked(cs, inputContext, attribute, controlFlags); } InputBindResult startInputUncheckedLocked(ClientState cs, IInputContext inputContext, EditorInfo attribute, int controlFlags) { //如果新程序和当前活动的程序不同 if (mCurClient != cs) { //取消当前活动程序和输入法的绑定 unbindCurrentClientLocked(); } //将新程序设置为当前活动的程序 mCurClient = cs; mCurInputContext = inputContext; mCurAttribute = attribute; if (mCurId != null && mCurId.equals(mCurMethodId)) { if (cs.curSession != null) { //连接已经建立,直接开始绑定 return attachNewInputLocked( (controlFlags&InputMethodManager.CONTROL_START_INITIAL) != 0); } if (mHaveConnection) { //输入法的连接是否已经创建,如果已经创建,直接传递给程序client端 if (mCurMethod != null) { requestClientSessionLocked(cs); return new InputBindResult(null, null, mCurId, mCurSeq); } } } //否则需要启动输入法,并建立连接 return startInputInnerLocked(); } InputBindResult startInputInnerLocked() { InputMethodInfo info = mMethodMap.get(mCurMethodId); unbindCurrentMethodLocked(false, true); //启动输入法service mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE); mCurIntent.setComponent(info.getComponent()); mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL, com.android.internal.R.string.input_method_binding_label); mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0)); if (bindCurrentInputMethodService(mCurIntent, this, Context.BIND_AUTO_CREATE | Context.BIND_NOT_VISIBLE | Context.BIND_SHOWING_UI)) { mHaveConnection = true; mCurId = info.getId(); //这个token是给输入法service用来绑定输入法的window的,通过这个token //InputMethodManagerService可以很方便的直接管理输入法的window mCurToken = new Binder(); try { mIWindowManager.addWindowToken(mCurToken, WindowManager.LayoutParams.TYPE_INPUT_METHOD); } catch (RemoteException e) { } return new InputBindResult(null, null, mCurId, mCurSeq); } return null; } private boolean bindCurrentInputMethodService( Intent service, ServiceConnection conn, int flags) { if (service == null || conn == null) { Slog.e(TAG, "--- bind failed: service = " + service + ", conn = " + conn); return false; } return mContext.bindServiceAsUser(service, conn, flags, new UserHandle(mSettings.getCurrentUserId())); } //输入法启动完成后就在函数onBind 传回一个binder接口 @Override final public IBinder onBind(Intent intent) { if (mInputMethod == null) { mInputMethod = onCreateInputMethodInterface(); } // IInputMethodWrapper只是一个wrapper,它负责将IMMS的调用转化为message //然后在message线程再调用mInputMethod对应的接口 //这样输入法的处理就是异步的了,因此你说它就是mInputMethod return new IInputMethodWrapper(this, mInputMethod); } @Override public AbstractInputMethodImpl onCreateInputMethodInterface() { return new InputMethodImpl(); } //由于IMMS是以bindService的方式启动输入法service,所以当输入法service启动完 //成后它就会回调IMMS的onServiceConnected @Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (mMethodMap) { if (mCurIntent != null && name.equals(mCurIntent.getComponent())) { //保存输入法service传递过来的通信接口IInputMethod mCurMethod = IInputMethod.Stub.asInterface(service); //将刚刚创建的window token传递给输入法service,然后输入用这个token //创建window,这样IMMS可以用根据这个token找到输入法在IMMS里 //的数据及输入法window在WMS里的数据 executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO( MSG_ATTACH_TOKEN, mCurMethod, mCurToken)); if (mCurClient != null) { //请求为程序和输入法建立一个连接会话,这样client就可以直接和 //输入法通信了 requestClientSessionLocked(mCurClient); } } } }
输入法Window token的绑定及使用分析
输入法Window token绑定
IMMS在输入法启动完成并回调onServiceConnected时会将一个Window token传递给输入法。
@Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (mMethodMap) { if (mCurIntent != null && name.equals(mCurIntent.getComponent())) { mCurMethod = IInputMethod.Stub.asInterface(service); executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO( MSG_ATTACH_TOKEN, mCurMethod, mCurToken)); if (mCurClient != null) { clearClientSessionLocked(mCurClient); requestClientSessionLocked(mCurClient); } } } } case MSG_ATTACH_TOKEN: args = (SomeArgs)msg.obj; try { //和输入法通信 ((IInputMethod)args.arg1).attachToken((IBinder)args.arg2); } catch (RemoteException e) { } args.recycle(); public class InputMethodService extends AbstractInputMethodService { public class InputMethodImpl extends AbstractInputMethodImpl { public void attachToken(IBinder token) { if (mToken == null) { //保存token mToken = token; //这样输入法的window就绑定这个window token mWindow.setToken(token); } } }
输入法Window token使用
由于系统存在多个输入法,所以输入法要和IMMS通信,必须要个机制来标示自己是哪个输入法,这个就是通过上面的输入法Window token来实现的,比如输入法自己关闭自己:
//InputMethodService.java输入法接口 public void requestHideSelf(int flags) { //mToken就是上面提到的过程----IMMS传递给输入法的 mImm.hideSoftInputFromInputMethod(mToken, flags); } //InputMethodManager.java public void hideSoftInputFromInputMethod(IBinder token, int flags) { try { mService.hideMySoftInput(token, flags); } catch (RemoteException e) { throw new RuntimeException(e); } } //IMMS @Override public void hideMySoftInput(IBinder token, int flags) { if (!calledFromValidUser()) { return; } synchronized (mMethodMap) { if (token == null || mCurToken != token) { if (DEBUG) Slog.w(TAG, "Ignoring hideInputMethod of uid " + Binder.getCallingUid() + " token: " + token); return; } long ident = Binder.clearCallingIdentity(); try { hideCurrentInputLocked(flags, null); } finally { Binder.restoreCallingIdentity(ident); } } }
输入法连接会话创建
到此程序和输入法的session就建立了
@Override public void onServiceConnected(ComponentName name, IBinder service) { synchronized (mMethodMap) { if (mCurIntent != null && name.equals(mCurIntent.getComponent())) { if (mCurClient != null) { clearClientSessionLocked(mCurClient); requestClientSessionLocked(mCurClient); } } } } void requestClientSessionLocked(ClientState cs) { if (!cs.sessionRequested) { //这里又出现了InputChannel对,很面熟吧,在前面几篇文章已经详细分析过 //了,可见它已经成为一种通用的跨平台的数据通信接口了 InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString()); cs.sessionRequested = true; executeOrSendMessage(mCurMethod, mCaller.obtainMessageOOO( MSG_CREATE_SESSION, mCurMethod, channels[1], new MethodCallback(this, mCurMethod, channels[0]))); } } case MSG_CREATE_SESSION: { args = (SomeArgs)msg.obj; IInputMethod method = (IInputMethod)args.arg1; InputChannel channel = (InputChannel)args.arg2; try { method.createSession(channel, (IInputSessionCallback)args.arg3); } catch (RemoteException e) { } //上面是IMMS端,下面就看IMS输入法端的处理 public abstract class AbstractInputMethodService extends Service implements KeyEvent.Callback { public abstract class AbstractInputMethodImpl implements InputMethod { public void createSession(SessionCallback callback) { callback.sessionCreated(onCreateInputMethodSessionInterface()); }<pre class="java" name="code"> }
}
传递输入法接口给程序
void onSessionCreated(IInputMethod method, IInputMethodSession session, InputChannel channel) { synchronized (mMethodMap) { if (mCurMethod != null && method != null && mCurMethod.asBinder() == method.asBinder()) { if (mCurClient != null) { InputBindResult res = attachNewInputLocked(true); if (res.method != null) { executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO( MSG_BIND_METHOD, mCurClient.client, res)); } return; } } } channel.dispose(); } case MSG_BIND_METHOD: { args = (SomeArgs)msg.obj; IInputMethodClient client = (IInputMethodClient)args.arg1; InputBindResult res = (InputBindResult)args.arg2; try { //会调回到程序端 client.onBindMethod(res); } args.recycle(); return true; }
输入法和view绑定
//IMMS InputBindResult attachNewInputLocked(boolean initial) { if (!mBoundToMethod) { executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO( MSG_BIND_INPUT, mCurMethod, mCurClient.binding)); mBoundToMethod = true; } final SessionState session = mCurClient.curSession; if (initial) { executeOrSendMessage(session.method, mCaller.obtainMessageOOO( MSG_START_INPUT, session, mCurInputContext, mCurAttribute)); } else { executeOrSendMessage(session.method, mCaller.obtainMessageOOO( MSG_RESTART_INPUT, session, mCurInputContext, mCurAttribute)); } return new InputBindResult(session.session, session.channel != null ? session.channel.dup() : null, mCurId, mCurSeq); } case MSG_BIND_INPUT: args = (SomeArgs)msg.obj; try { ((IInputMethod)args.arg1).bindInput((InputBinding)args.arg2); } catch (RemoteException e) { } args.recycle(); return true; case MSG_START_INPUT: args = (SomeArgs)msg.obj; try { SessionState session = (SessionState)args.arg1; session.method.startInput((IInputContext)args.arg2, (EditorInfo)args.arg3); } catch (RemoteException e) { } args.recycle(); return true; //IMS @Override public void startInput(IInputContext inputContext, EditorInfo attribute) { mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_START_INPUT, inputContext, attribute)); } case DO_START_INPUT: { SomeArgs args = (SomeArgs)msg.obj; // IInputContext就是输入法和文本输入view的通信接口 //通过这个接口,输入法能够获取view的信息,也能够直接将文本传 //送给view IInputContext inputContext = (IInputContext)args.arg1; InputConnection ic = inputContext != null ? new InputConnectionWrapper(inputContext) : null; EditorInfo info = (EditorInfo)args.arg2; inputMethod.startInput(ic, info); args.recycle(); return; } public class InputMethodImpl extends AbstractInputMethodImpl { public void startInput(InputConnection ic, EditorInfo attribute) { doStartInput(ic, attribute, false); } } void doStartInput(InputConnection ic, EditorInfo attribute, boolean restarting) { if (!restarting) { doFinishInput(); } mInputStarted = true; mStartedInputConnection = ic; mInputEditorInfo = attribute; initialize(); onStartInput(attribute, restarting); if (mWindowVisible) { if (mShowInputRequested) { mInputViewStarted = true; //真正的输入法需要在这个接口里实现输入法的内容 onStartInputView(mInputEditorInfo, restarting); startExtractingText(true); } else if (mCandidatesVisibility == View.VISIBLE) { mCandidatesViewStarted = true; onStartCandidatesView(mInputEditorInfo, restarting); } } }
到此焦点view已经通过调用IMMS的startInput和输入法绑定了,但是此时输入法还没有显示。但是系统紧接着会调用windowGainFocus来显示输入法。
程序焦点获取事件导致输入法显示
输入法响应显示请求
用户单击输入框View导致输入法显示
输入法传递输入文本信息给view
/********************************
* 本文来自博客 “爱踢门”
* 转载请标明出处:http://blog.csdn.net/itleaks
******************************************/
- Android输入法框架系统(上)
- Android输入法框架系统(上)
- Android输入法框架系统(下)
- Android输入法框架系统(下)
- Android 的输入法框架
- android 输入法 框架
- Android输入法框架
- android输入法框架整理
- Android输入法框架整理
- Android输入法框架整理
- Android 输入法框架简介
- Android专题-----输入法框架
- Android的输入法框架
- Android输入法框架分析
- Android调用系统输入法
- Android调用系统输入法
- android 输入法框架(未完成)
- Android输入法之输入系统
- [leetcode] Reverse Nodes in k-Group
- 黑马程序员:java基础知识(二)
- 时光轴
- 关于||和&&的一个有趣问题
- subsys_initcall
- Android输入法框架系统(上)
- IOS_网络请求_get+post+同步+异步
- android -- 蓝牙 bluetooth (一) 入门
- 多边形背景生成工具推荐-Trianglify
- HDU 最短路小小结(只涉及初级算法)
- android -- 蓝牙 bluetooth (二) 打开蓝牙
- PDB文件:每个开发人员都必须知道的
- debian 安装ruby +nginx+apache开发环境.
- Android Activity 生命周期的透彻理解