Android 输入法框架 (2)

编程入门 行业动态 更新时间:2024-10-25 06:29:20

Android <a href=https://www.elefans.com/category/jswz/34/1767193.html style=输入法框架 (2)"/>

Android 输入法框架 (2)

通常显示或者隐藏输入法有以下三个场景

  • 当一个窗口焦点改变的时候,会根据Window属性控制输入法的显示
  • App主动调用imm::showSoftInput或者imm::hideSoftinputFromWindow
  • 输入法自己调用ims:requestShowSelf或者ims:requestHideSelf

Window焦点改变

当window获得焦点时会调用imm::onPostWindowFocus方法

 /*** Called by ViewAncestor when its window gets input focus.* @hide*/public void onPostWindowFocus(View rootView, View focusedView,@SoftInputModeFlags int softInputMode, boolean first, int windowFlags) {boolean forceNewFocus = false;// 对焦点view的控制,后续分析synchronized (mH) {if (DEBUG) Log.v(TAG, "onWindowFocus: " + focusedView+ " softInputMode=" + InputMethodClient.softInputModeToString(softInputMode)+ " first=" + first + " flags=#"+ Integer.toHexString(windowFlags));if (mRestartOnNextWindowFocus) {if (DEBUG) Log.v(TAG, "Restarting due to mRestartOnNextWindowFocus");mRestartOnNextWindowFocus = false;forceNewFocus = true;}focusInLocked(focusedView != null ? focusedView : rootView);}.......// 该方法是对焦点view的判断,这里默认返回trueif (checkFocusNoStartInput(forceNewFocus)) {// We need to restart input on the current focus view.  This// should be done in conjunction with telling the system service// about the window gaining focus, to help make the transition// smooth.if (startInputInner(InputMethodClient.START_INPUT_REASON_WINDOW_FOCUS_GAIN,rootView.getWindowToken(), controlFlags, softInputMode, windowFlags)) {return;}}......}

中间会有一些焦点view的判断,后续分析,这里会进入startInputInner方法

 boolean startInputInner(@InputMethodClient.StartInputReason final int startInputReason,IBinder windowGainingFocus, int controlFlags, int softInputMode,int windowFlags) {......// 在这里创建了焦点View的EditorInfo和InputConnectionEditorInfo tba = new EditorInfo();tba.packageName = view.getContext().getOpPackageName();tba.fieldId = view.getId();InputConnection ic = view.onCreateInputConnection(tba);if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);synchronized (mH) {......// InputConnection被封装成ControlledInputConnectionWrapper,然后被赋值给mServedInputConnectionWrapper对象ControlledInputConnectionWrapper servedContext;final int missingMethodFlags;if (ic != null) {......servedContext = new ControlledInputConnectionWrapper(icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this);} else {servedContext = null;missingMethodFlags = 0;}mServedInputConnectionWrapper = servedContext;try {// 调用imms方法final InputBindResult res = mService.startInputOrWindowGainedFocus(startInputReason, mClient, windowGainingFocus, controlFlags, softInputMode,windowFlags, tba, servedContext, missingMethodFlags,view.getContext().getApplicationInfo().targetSdkVersion);......} catch (RemoteException e) {Log.w(TAG, "IME died: " + mCurId, e);}}return true;}

在startInputInner中创建当前焦点View的EditorInfo和获取焦点View的InputConnection,在传递给imms前会被封装成ControlledInputConnectionWrapper,然后就从app进程进入了system进程

    @NonNull@Overridepublic InputBindResult startInputOrWindowGainedFocus(/* @InputMethodClient.StartInputReason */ final int startInputReason,IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode,int windowFlags, @Nullable EditorInfo attribute, IInputContext inputContext,/* @InputConnectionInspector.missingMethods */ final int missingMethods,int unverifiedTargetSdkVersion) {final InputBindResult result;if (windowToken != null) {result = windowGainedFocus(startInputReason, client, windowToken, controlFlags,softInputMode, windowFlags, attribute, inputContext, missingMethods,unverifiedTargetSdkVersion);} else {result = startInput(startInputReason, client, inputContext, missingMethods, attribute,controlFlags);}if (result == null) {// This must never happen, but just in case.Slog.wtf(TAG, "InputBindResult is @NonNull. startInputReason="+ InputMethodClient.getStartInputReason(startInputReason)+ " windowFlags=#" + Integer.toHexString(windowFlags)+ " editorInfo=" + attribute);return InputBindResult.NULL;}return result;}@NonNullprivate InputBindResult windowGainedFocus(/* @InputMethodClient.StartInputReason */ final int startInputReason,IInputMethodClient client, IBinder windowToken, int controlFlags,/* @android.view.WindowManager.LayoutParams.SoftInputModeFlags */ int softInputMode,int windowFlags, EditorInfo attribute, IInputContext inputContext,/* @InputConnectionInspector.missingMethods */  final int missingMethods,int unverifiedTargetSdkVersion) {// Needs to check the validity before clearing calling identityfinal boolean calledFromValidUser = calledFromValidUser();InputBindResult res = null;long ident = Binder.clearCallingIdentity();try {.......synchronized (mMethodMap) {     // is more room for the target window + IME.final boolean doAutoShow =(softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST)== WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|| mRes.getConfiguration().isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);final boolean isTextEditor =(controlFlags&InputMethodManager.CONTROL_WINDOW_IS_TEXT_EDITOR) != 0;			// We want to start input before showing the IME, but after closing// it.  We want to do this after closing it to help the IME disappear// more quickly (not get stuck behind it initializing itself for the// new focused input, even if its window wants to hide the IME).boolean didStart = false;switch (softInputMode&WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) {case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:if (!isTextEditor || !doAutoShow) {if (WindowManager.LayoutParams.mayUseInputMethod(windowFlags)) {// There is no focus view, and this window will// be behind any soft input window, so hide the// soft input window if it is shown.if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");hideCurrentInputLocked(InputMethodManager.HIDE_NOT_ALWAYS, null);}} else if (isTextEditor && doAutoShow && (softInputMode &WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {// There is a focus view, and we are navigating forward// into the window, so show the input window for the user.// We only do this automatically if the window can resize// to accommodate the IME (so what the user sees will give// them good context without input information being obscured// by the IME) or if running on a large screen where there// is more room for the target window + IME.if (DEBUG) Slog.v(TAG, "Unspecified window will show input");if (attribute != null) {res = startInputUncheckedLocked(cs, inputContext,missingMethods, attribute, controlFlags, startInputReason);didStart = true;}showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);}break;case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED:// Do nothing.break;case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:if ((softInputMode &WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");hideCurrentInputLocked(0, null);}break;case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:if (DEBUG) Slog.v(TAG, "Window asks to hide input");hideCurrentInputLocked(0, null);break;case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:if ((softInputMode &WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, controlFlags)) {if (attribute != null) {res = startInputUncheckedLocked(cs, inputContext,missingMethods, attribute, controlFlags,startInputReason);didStart = true;}showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);} else {Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"+ " there is no focused view that also returns true from"+ " View#onCheckIsTextEditor()");}}break;case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:if (DEBUG) Slog.v(TAG, "Window asks to always show input");if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, controlFlags)) {if (attribute != null) {res = startInputUncheckedLocked(cs, inputContext, missingMethods,attribute, controlFlags, startInputReason);didStart = true;}showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);} else {Slog.e(TAG, "SOFT_INPUT_STATE_ALWAYS_VISIBLE is ignored because"+ " there is no focused view that also returns true from"+ " View#onCheckIsTextEditor()");}break;}if (!didStart) {if (attribute != null) {if (!DebugFlags.FLAG_OPTIMIZE_START_INPUT.value()|| (controlFlags& InputMethodManager.CONTROL_WINDOW_IS_TEXT_EDITOR) != 0) {res = startInputUncheckedLocked(cs, inputContext, missingMethods,attribute,controlFlags, startInputReason);} else {res = InputBindResult.NO_EDITOR;}} else {res = InputBindResult.NULL_EDITOR_INFO;}}}} finally {Binder.restoreCallingIdentity(ident);}return res;}

startInputOrWindowGainedFocus和windowGainedFocus方法最重要的地方是判断window的softInputMode

这里有两个重要的变量

isTextEditor 通过imm传过来的controlFlags参数判断当前焦点view是否是编辑框

doAutoShow 配置的softInputMode是否带有WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE参数

Window的softInputMode我们可以在manifest中注册或者动态通过代码设置

AndroidManifest android:windowSoftInputMode="stateHidden"
Java            getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE );
  • WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED (默认参数)
  1. 如果isTextEditor或者doAutoShow 有任一个为false 会隐藏输入法
  2. 如果都为true 则会显示输入法
  • WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED

         不作任何处理

  • WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN

        隐藏输入法且带有SOFT_INPUT_IS_FORWARD_NAVIGATION参数

  • WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN

        隐藏输入法,和上面的区别是不会判断SOFT_INPUT_IS_FORWARD_NAVIGATION参数

  • WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE

        显示输入法且带有SOFT_INPUT_IS_FORWARD_NAVIGATION参数

  • WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE

        显示输入法,和上面的区别是不会判断SOFT_INPUT_STATE_ALWAYS_VISIBLE

注意: WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION 是根据AMS中这个方法的返回值决定的

    boolean isNextTransitionForward() {int transit = mWindowManager.getPendingAppTransition();return transit == TRANSIT_ACTIVITY_OPEN|| transit == TRANSIT_TASK_OPEN|| transit == TRANSIT_TASK_TO_FRONT;}

接着首先分析显示流程

如果当前EditorInfo不为null(通常不为null) 则会先走startInputUncheckedLocked方法

 

    @GuardedBy("mMethodMap")@NonNullInputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,/* @InputConnectionInspector.missingMethods */ final int missingMethods,@NonNull EditorInfo attribute, int controlFlags,/* @InputMethodClient.StartInputReason */ final int startInputReason) {......// 如果是从其他app跳转过来的 先解绑之前的client,然后再绑定新的cleintif (mCurClient != cs) {// Was the keyguard locked when switching over to the new client?mCurClientInKeyguard = isKeyguardLocked();// If the client is changing, we need to switch over to the new// one.unbindCurrentClientLocked(InputMethodClient.UNBIND_REASON_SWITCH_CLIENT);if (DEBUG) Slog.v(TAG, "switching to client: client="+ cs.client.asBinder() + " keyguard=" + mCurClientInKeyguard);// If the screen is on, inform the new client it is activeif (mIsInteractive) {executeOrSendMessage(cs.client, mCaller.obtainMessageIO(MSG_SET_ACTIVE, mIsInteractive ? 1 : 0, cs));}}// Bump up the sequence for this client and attach it.mCurSeq++;if (mCurSeq <= 0) mCurSeq = 1;mCurClient = cs;mCurInputContext = inputContext;mCurInputContextMissingMethods = missingMethods;mCurAttribute = attribute;// Check if the input method is changing.if (mCurId != null && mCurId.equals(mCurMethodId)) {if (cs.curSession != null) {// Fast case: if we are already connected to the input method,// then just return it.return attachNewInputLocked(startInputReason,(controlFlags&InputMethodManager.CONTROL_START_INITIAL) != 0);}......}return startInputInnerLocked();}

在imms内部每一个app对应一个client,如果从不同的app切换跳转过来会先解绑之前的client,解绑过程会回调ims的unbindInput方法和之前client的imm的onUnbindMethod方法和setActive(false)方法,然后再调用新的client的setActive(true)方法。

接着会调用attachNewInputLocked方法

如果imms没有绑定当前client,即从其他app跳转过来的,则会在这个方法里绑定一次

    @GuardedBy("mMethodMap")@NonNullInputBindResult attachNewInputLocked(/* @InputMethodClient.StartInputReason */ final int startInputReason, boolean initial) {if (!mBoundToMethod) {executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(MSG_BIND_INPUT, mCurMethod, mCurClient.binding));mBoundToMethod = true;}final Binder startInputToken = new Binder();final StartInputInfo info = new StartInputInfo(mCurToken, mCurId, startInputReason,!initial, mCurFocusedWindow, mCurAttribute, mCurFocusedWindowSoftInputMode,mCurSeq);mStartInputMap.put(startInputToken, info);mStartInputHistory.addEntry(info);final SessionState session = mCurClient.curSession;executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO(MSG_START_INPUT, mCurInputContextMissingMethods, initial ? 0 : 1 /* restarting */,startInputToken, session, mCurInputContext, mCurAttribute));if (mShowRequested) {if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");showCurrentInputLocked(getAppShowFlags(), null);}return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,session.session, (session.channel != null ? session.channel.dup() : null),mCurId, mCurSeq, mCurUserActionNotificationSequenceNumber);}

然后会回调ims的bindInput方法和onStartInput方法,注意这个有一个属性mShowRequested,如果之前输入法是显示状态,则这个值为true,否则为false。

在window焦点改变且显示输入法的情况下,即使这里会fasle了,后面也会再次调用showCurrentInputLocked方法

    boolean showCurrentInputLocked(int flags, ResultReceiver resultReceiver) {mShowRequested = true;if (mAccessibilityRequestingNoSoftKeyboard) {return false;}if ((flags&InputMethodManager.SHOW_FORCED) != 0) {mShowExplicitlyRequested = true;mShowForced = true;} else if ((flags&InputMethodManager.SHOW_IMPLICIT) == 0) {mShowExplicitlyRequested = true;}if (!mSystemReady) {return false;}boolean res = false;if (mCurMethod != null) {if (DEBUG) Slog.d(TAG, "showCurrentInputLocked: mCurToken=" + mCurToken);executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(MSG_SHOW_SOFT_INPUT, getImeShowFlags(), mCurMethod,resultReceiver));mInputShown = true;if (mHaveConnection && !mVisibleBound) {bindCurrentInputMethodServiceLocked(mCurIntent, mVisibleConnection, IME_VISIBLE_BIND_FLAGS);mVisibleBound = true;}res = true;} else if (mHaveConnection && SystemClock.uptimeMillis()>= (mLastBindTime+TIME_TO_RECONNECT)) {// The client has asked to have the input method shown, but// we have been sitting here too long with a connection to the// service and no interface received, so let's disconnect/connect// to try to prod things along.EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, mCurMethodId,SystemClock.uptimeMillis()-mLastBindTime,1);Slog.w(TAG, "Force disconnect/connect to the IME in showCurrentInputLocked()");mContext.unbindService(this);bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS);} else {if (DEBUG) {Slog.d(TAG, "Can't show input: connection = " + mHaveConnection + ", time = "+ ((mLastBindTime+TIME_TO_RECONNECT) - SystemClock.uptimeMillis()));}}return res;}

这个方法比较简单,正常情况下这里会回调ims的showSoftInput方法,只不过需要注意mShowForced和mShowExplicitlyRequested这两个属性

如果flag带有InputMethodManager.SHOW_FORCED,则这里mShowForced和mShowExplicitlyRequested均置为true。

如果flag带有InputMethodManager.SHOW_IMPLICIT,则这里mShowExplicitlyRequested置为true。

显示输入法的流程在此就结束了,ims的相关流程会在之后分析。

 

接下来分析隐藏输入法的流程,比较简单

boolean hideCurrentInputLocked(int flags, ResultReceiver resultReceiver) {if ((flags&InputMethodManager.HIDE_IMPLICIT_ONLY) != 0&& (mShowExplicitlyRequested || mShowForced)) {if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");return false;}if (mShowForced && (flags&InputMethodManager.HIDE_NOT_ALWAYS) != 0) {if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide");return false;}// There is a chance that IMM#hideSoftInput() is called in a transient state where// IMMS#InputShown is already updated to be true whereas IMMS#mImeWindowVis is still waiting// to be updated with the new value sent from IME process.  Even in such a transient state// historically we have accepted an incoming call of IMM#hideSoftInput() from the// application process as a valid request, and have even promised such a behavior with CTS// since Android Eclair.  That's why we need to accept IMM#hideSoftInput() even when only// IMMS#InputShown indicates that the software keyboard is shown.// TODO: Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested.final boolean shouldHideSoftInput = (mCurMethod != null) && (mInputShown ||(mImeWindowVis & InputMethodService.IME_ACTIVE) != 0);boolean res;if (shouldHideSoftInput) {// The IME will report its visible state again after the following message finally// delivered to the IME process as an IPC.  Hence the inconsistency between// IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in// the final state.executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(MSG_HIDE_SOFT_INPUT, mCurMethod, resultReceiver));res = true;} else {res = false;}if (mHaveConnection && mVisibleBound) {mContext.unbindService(mVisibleConnection);mVisibleBound = false;}mInputShown = false;mShowRequested = false;mShowExplicitlyRequested = false;mShowForced = false;return res;}

这里看到如果mShowForced或者mShowExplicitlyRequested为true,传入的flag必须具有对应的flag,否则会直接return。

正常流程下会调用ims的hideSoftInput方法

imm::showSoftInput

imm::showSoftInput -> imms:showSoftinput -> imms:showCurrentInputLocked 流程比较简单,同之前的流程

immpublic boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {checkFocus();synchronized (mH) {if (mServedView != view && (mServedView == null|| !mServedView.checkInputConnectionProxy(view))) {return false;}try {return mService.showSoftInput(mClient, flags, resultReceiver);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}}imms@Overridepublic boolean showSoftInput(IInputMethodClient client, int flags,ResultReceiver resultReceiver) {if (!calledFromValidUser()) {return false;}int uid = Binder.getCallingUid();long ident = Binder.clearCallingIdentity();try {synchronized (mMethodMap) {if (mCurClient == null || client == null|| mCurClient.client.asBinder() != client.asBinder()) {try {// We need to check if this is the current client with// focus in the window manager, to allow this call to// be made before input is started in it.if (!mIWindowManager.inputMethodClientHasFocus(client)) {Slog.w(TAG, "Ignoring showSoftInput of uid " + uid + ": " + client);return false;}} catch (RemoteException e) {return false;}}if (DEBUG) Slog.v(TAG, "Client requesting input be shown");return showCurrentInputLocked(flags, resultReceiver);}} finally {Binder.restoreCallingIdentity(ident);}}

 

imm::hideSoftInputFromWindow

imm::hideSoftInputFromWinow -> imms::hideSoftinput -> imms::hideCurrentInputLocked 同之前的流程

immpublic boolean hideSoftInputFromWindow(IBinder windowToken, int flags,ResultReceiver resultReceiver) {checkFocus();synchronized (mH) {if (mServedView == null || mServedView.getWindowToken() != windowToken) {return false;}try {return mService.hideSoftInput(mClient, flags, resultReceiver);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}}imms@Overridepublic boolean hideSoftInput(IInputMethodClient client, int flags,ResultReceiver resultReceiver) {if (!calledFromValidUser()) {return false;}int uid = Binder.getCallingUid();long ident = Binder.clearCallingIdentity();try {synchronized (mMethodMap) {if (mCurClient == null || client == null|| mCurClient.client.asBinder() != client.asBinder()) {try {// We need to check if this is the current client with// focus in the window manager, to allow this call to// be made before input is started in it.if (!mIWindowManager.inputMethodClientHasFocus(client)) {if (DEBUG) Slog.w(TAG, "Ignoring hideSoftInput of uid "+ uid + ": " + client);return false;}} catch (RemoteException e) {return false;}}if (DEBUG) Slog.v(TAG, "Client requesting input be hidden");return hideCurrentInputLocked(flags, resultReceiver);}} finally {Binder.restoreCallingIdentity(ident);}}

ims:requestShowSelf

ims:requestShowSelf -> mImm.showSoftInputFromInputMethodInternal -> imms:showMySoftInput

imspublic final void requestShowSelf(int flags) {mImm.showSoftInputFromInputMethodInternal(mToken, flags);}immpublic void showSoftInputFromInputMethodInternal(IBinder token, int flags) {try {mService.showMySoftInput(token, flags);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}imms@Overridepublic void showMySoftInput(IBinder token, int flags) {if (!calledFromValidUser()) {return;}synchronized (mMethodMap) {if (!calledWithValidToken(token)) {return;}long ident = Binder.clearCallingIdentity();try {showCurrentInputLocked(flags, null);} finally {Binder.restoreCallingIdentity(ident);}}}

除了以上三个方法,还有其他的比如toggelSoftInput或者restartInput等等,实际流程都差不多~

 

更多推荐

Android 输入法框架 (2)

本文发布于:2023-06-22 05:11:43,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/828211.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:输入法   框架   Android

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!