歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Android IMF 分析

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();  //完成編輯輸入  
    }          

 接口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">  

在這個接口中有

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;  

    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);  

->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);  
            }  
        }  

 那麼到了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();  
               }  
           }  
       }  

 doStartInput->initialize->onInitializeInterface->onStartInput->onStartInputView.

onStartInputView中會handle各種IME的event並進一步調用IME自身的onCreate函數,做進一步的初始化以及receiver的rigister。

Copyright © Linux教程網 All Rights Reserved