输入法

本文详细解读了Android输入法框架IMF的内部机制,包括其核心组件如SoftInputView、CandidatesView及字符输出流程,以及如何通过InputConnection接口实现与输入法之间的交互。此外,阐述了IME服务的启动过程与IME部分的处理逻辑,包括配置变更事件的响应、输入法的初始化与显示等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

输入法

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 之间通信的通道,包括实现读取关标周围的输入,向文本框中输入文本以及给应用程序发送各种按键事件。

Java代码 复制代码  收藏代码
  1. InputConnection ic = getCurrentInputConnection();   //获取InputConnection接口   
  2.   
  3. if (ic != null){   
  4.     ic.beginBatchEdit();    //开始输入编辑操作                     
  5.     if (isShifted()) {   
  6.         text = text.toString().toUpperCase();   
  7.     }                      
  8.     ic.commitText(text, 1); //将text字符输入文本框,并且将关标移至字符做插入态   
  9.     ic.endBatchEdit();  //完成编辑输入   
  10. }          
        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。

Xml代码 复制代码  收藏代码
  1. <service android:name="IME"  
  2.         android:label="@string/SoftkeyIME"  
  3.         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中取到的代码,可以比较全面的反应这个接口的使用:

Java代码 复制代码  收藏代码
  1. final InputMethodSession mInputMethodSession;  
 final InputMethodSession mInputMethodSession;
Java代码 复制代码  收藏代码
  1. public void executeMessage(Message msg) {   
  2.         switch (msg.what) {   
  3.             case DO_FINISH_INPUT:   
  4.                 mInputMethodSession.finishInput();  //应用程序停止接收字符   
  5.                 return;   
  6.             case DO_DISPLAY_COMPLETIONS:   
  7.                 mInputMethodSession.displayCompletions((CompletionInfo[])msg.obj);  //输入法自动完成功能   
  8.                 return;   
  9.             case DO_UPDATE_EXTRACTED_TEXT:   
  10.                 mInputMethodSession.updateExtractedText(msg.arg1,   
  11.                         (ExtractedText)msg.obj);   
  12.                 return;   
  13.             case DO_DISPATCH_KEY_EVENT: {   
  14.                 HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj;   
  15.                 mInputMethodSession.dispatchKeyEvent(msg.arg1,   
  16.                         (KeyEvent)args.arg1,   
  17.                         new InputMethodEventCallbackWrapper(   
  18.                                 (IInputMethodCallback)args.arg2));  //处理按键   
  19.                 mCaller.recycleArgs(args);   
  20.                 return;   
  21.             }   
  22.             case DO_DISPATCH_TRACKBALL_EVENT: {   
  23.                 HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj;   
  24.                 mInputMethodSession.dispatchTrackballEvent(msg.arg1,   
  25.                         (MotionEvent)args.arg1,   
  26.                         new InputMethodEventCallbackWrapper(   
  27.                                 (IInputMethodCallback)args.arg2));    
  28.                 mCaller.recycleArgs(args);   
  29.                 return;   
  30.             }   
  31.             case DO_UPDATE_SELECTION: {   
  32.                 HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj;   
  33.                 mInputMethodSession.updateSelection(args.argi1, args.argi2,   
  34.                         args.argi3, args.argi4, args.argi5, args.argi6); //更新选取的字符   
  35.                 mCaller.recycleArgs(args);   
  36.                 return;   
  37.             }   
  38.             case DO_UPDATE_CURSOR: {   
  39.                 mInputMethodSession.updateCursor((Rect)msg.obj); //更新关标   
  40.                 return;   
  41.             }   
  42.             case DO_APP_PRIVATE_COMMAND: {   
  43.                 HandlerCaller.SomeArgs args = (HandlerCaller.SomeArgs)msg.obj;   
  44.                 mInputMethodSession.appPrivateCommand((String)args.arg1,   
  45.                         (Bundle)args.arg2); //处理应用程序发给输入法的命令   
  46.                 mCaller.recycleArgs(args);   
  47.                 return;   
  48.             }   
  49.             case DO_TOGGLE_SOFT_INPUT: {   
  50.                 mInputMethodSession.toggleSoftInput(msg.arg1, msg.arg2); //切换软键盘   
  51.                 return;   
  52.             }   
  53.         }   
  54.         Log.w(TAG, "Unhandled message code: " + msg.what);   
  55.     }  

我们知道当一个编辑框获得焦点的时候,将会create一个新的IME客户端,那么IMF框架是怎样处理这个事件的呢?

 

首先调用EditText的构造函数EditTex->TextView->setText()           

Java代码 复制代码  收藏代码
  1. InputMethodManager imm = InputMethodManager.peekInstance();   
  2. if (imm != null) imm.restartInput(this);  
 InputMethodManager imm = InputMethodManager.peekInstance();
 if (imm != null) imm.restartInput(this);

 

->startInputInner

 

Java代码 复制代码  收藏代码
  1. void startInputInner() {   
  2.     final View view;   
  3.     synchronized (mH) {   
  4.         view = mServedView;   
  5.            
  6.         // Make sure we have a window token for the served view.   
  7.         if (DEBUG) Log.v(TAG, "Starting input: view=" + view);   
  8.         if (view == null) {   
  9.             if (DEBUG) Log.v(TAG, "ABORT input: no served view!");   
  10.             return;   
  11.         }   
  12.     }   
  13.        
  14.     // Now we need to get an input connection from the served view.   
  15.     // This is complicated in a couple ways: we can't be holding our lock   
  16.     // when calling out to the view, and we need to make sure we call into   
  17.     // the view on the same thread that is driving its view hierarchy.   
  18.     Handler vh = view.getHandler();   
  19.     if (vh == null) {   
  20.         // If the view doesn't have a handler, something has changed out   
  21.         // from under us, so just bail.   
  22.         if (DEBUG) Log.v(TAG, "ABORT input: no handler for view!");   
  23.         return;   
  24.     }   
  25.     if (vh.getLooper() != Looper.myLooper()) {   
  26.         // The view is running on a different thread than our own, so   
  27.         // we need to reschedule our work for over there.   
  28.         if (DEBUG) Log.v(TAG, "Starting input: reschedule to view thread");   
  29.         vh.post(new Runnable() {   
  30.             public void run() {   
  31.                 startInputInner();   
  32.             }   
  33.         });   
  34.         return;   
  35.     }   
  36.        
  37.     // Okay we are now ready to call into the served view and have it   
  38.     // do its stuff.   
  39.     // Life is good: let's hook everything up!   
  40.     EditorInfo tba = new EditorInfo();   
  41.     tba.packageName = view.getContext().getPackageName();   
  42.     tba.fieldId = view.getId();   
  43.     InputConnection ic = view.onCreateInputConnection(tba); //create 新的InputConnection    
  44.     if (DEBUG) Log.v(TAG, "Starting input: tba=" + tba + " ic=" + ic);   
  45.        
  46.     synchronized (mH) {   
  47.         // Now that we are locked again, validate that our state hasn't   
  48.         // changed.   
  49.         if (mServedView != view || !mServedConnecting) {   
  50.             // Something else happened, so abort.   
  51.             if (DEBUG) Log.v(TAG,    
  52.                     "Starting input: finished by someone else (view="  
  53.                     + mServedView + " conn=" + mServedConnecting + ")");   
  54.             return;   
  55.         }   
  56.            
  57.         // If we already have a text box, then this view is already   
  58.         // connected so we want to restart it.   
  59.         final boolean initial = mCurrentTextBoxAttribute == null;   
  60.            
  61.         // Hook 'em up and let 'er rip.   
  62.         mCurrentTextBoxAttribute = tba;   
  63.         mServedConnecting = false;   
  64.         mServedInputConnection = ic;   
  65.         IInputContext servedContext;   
  66.         if (ic != null) {   
  67.             mCursorSelStart = tba.initialSelStart;   
  68.             mCursorSelEnd = tba.initialSelEnd;   
  69.             mCursorCandStart = -1;   
  70.             mCursorCandEnd = -1;   
  71.             mCursorRect.setEmpty();   
  72.             servedContext = new ControlledInputConnectionWrapper(vh.getLooper(), ic);   
  73.         } else {   
  74.             servedContext = null;   
  75.         }   
  76.            
  77.         try {   
  78.             if (DEBUG) Log.v(TAG, "START INPUT: " + view + " ic="  
  79.                     + ic + " tba=" + tba + " initial=" + initial);   
  80.             InputBindResult res = mService.startInput(mClient,   
  81.                     servedContext, tba, initial, mCurMethod == null); //启动IMEservice   
  82.             if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);   
  83.             if (res != null) {   
  84.                 if (res.id != null) {   
  85.                     mBindSequence = res.sequence;   
  86.                     mCurMethod = res.method;   
  87.                 } else {   
  88.                     // This means there is no input method available.   
  89.                     if (DEBUG) Log.v(TAG, "ABORT input: no input method!");   
  90.                     return;   
  91.                 }   
  92.             }   
  93.             if (mCurMethod != null && mCompletions != null) {   
  94.                 try {   
  95.                     mCurMethod.displayCompletions(mCompletions);   
  96.                 } catch (RemoteException e) {   
  97.                 }   
  98.             }   
  99.         } catch (RemoteException e) {   
  100.             Log.w(TAG, "IME died: " + mCurId, e);   
  101.         }   
  102.     }   
    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)

Java代码 复制代码  收藏代码
  1. @Override public void onConfigurationChanged(Configuration newConfig) {   
  2.        super.onConfigurationChanged(newConfig);   
  3.           
  4.        boolean visible = mWindowVisible;   
  5.        int showFlags = mShowInputFlags;   
  6.        boolean showingInput = mShowInputRequested;   
  7.        CompletionInfo[] completions = mCurCompletions;   
  8.        initViews();   
  9.        mInputViewStarted = false;   
  10.        mCandidatesViewStarted = false;   
  11.        if (mInputStarted) {   
  12.            doStartInput(getCurrentInputConnection(),   
  13.                    getCurrentInputEditorInfo(), true);  //调用startinput,创建IME的input   
  14.        }   
  15.        if (visible) {   
  16.            if (showingInput) {   
  17.                // If we were last showing the soft keyboard, try to do so again.   
  18.                if (onShowInputRequested(showFlags, true)) {   
  19.                    showWindow(true);   
  20.                    if (completions != null) {   
  21.                        mCurCompletions = completions;   
  22.                        onDisplayCompletions(completions);   
  23.                    }   
  24.                } else {   
  25.                    hideWindow();   
  26.                }   
  27.            } else if (mCandidatesVisibility == View.VISIBLE) {   
  28.                // If the candidates are currently visible, make sure the   
  29.                // window is shown for them.   
  30.                showWindow(false);   
  31.            } else {   
  32.                // Otherwise hide the window.   
  33.                hideWindow();   
  34.            }   
  35.        }   
  36.    }  
 @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。

 

 

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值