Android輸入法擴展之外接鍵盤中文輸入


        大家想不想要這樣一台Android  Surface平板,看着就過癮吧。

                

     

         我們知道,android眼下的輸入都是通過軟鍵盤實現的,用外接鍵盤的少。這個在手機上是能夠理解的。當手機接上外接鍵盤后。總體會顯得頭重腳輕。而且用鍵盤輸入時。人離手機的距離就遠了,自然不太適合看清手機上的內容。那在平板上呢?假設平板僅僅是平時用來瀏覽看視頻,不進行大量輸入。自然也用不上外接鍵盤。

那到底什么時候須要用到外接鍵盤呢?本人認為首先要滿足例如以下兩個條件。

1)   平板和外接鍵盤完美融合,組合后非常像筆記本使用模式。類似上面Android Surface的機器,平板和鍵盤通過磁性自己主動粘合,變身筆記本模式

2)    Android用在類辦公等須要高速輸入場景,比方寫文章。長時間聊qq等。事實上linux一直以來沒法進入桌面系統的關鍵原因是window在這方面太優秀,它壟斷了用戶的辦公習慣,即用Microsoft office系列軟件辦公。可是如今類linux。尤其Android在這邊已經有了非常大進步,一方面,ubuntu幫組linux積累了一部分用戶。比方libre office體驗好多了。同一時候據說微軟正在為Android開發Microsoft office的響應產品,這個是利好消息。

     從上面看來。事實上市面上已經有滿足上面兩個條件的機器了。比方聯想的A10        

      它是一台超級本, 但它支持翻轉,當翻轉過來就是平板。

      那為啥這樣的Android超極本就不夠火呢?當然有非常多原因啊,比方平板本身需求量小,Android本身就不適合辦公。當然肯定也有另外一個小原因。它這個物理鍵盤居然不能中文輸入。

因此,Android平板要進入辦公領域並流行,須要實現類似PC端中文輸入的體驗。

     本文說到的外接鍵盤中文輸入,重在中文兩字。其實,Android本身是支持外接鍵盤的。可是僅僅可以實現英文輸入。其實。我們在前幾篇文章已經說到了輸入法,也已經分析到,Android要想輸入中文,必須通過輸入法。

那為啥Android的中文輸入法不能像PC那樣直接通過外接鍵盤輸入呢?以下一一分析。

 


Android沒法通過外接鍵盤中文輸入原因

 

輸入法和外接鍵盤不能共存

        Android系統里,當有外接鍵盤時。輸入法就會消失。這樣自然沒法通過輸入法輸入中文。

這個是由Configuration的keyboard配置項決定的。正常情況下。Configuration的keyboard值是nokeys,而當系統檢測到外接鍵盤(藍牙鍵盤等等)插入時,就會更新系統的Configuration,並將當中的keyboard置為非nokeys(比方Configuration.KEYBOARD_QWERTY),然后系統會將新的Configuration通知給全部程序,包含輸入法。

當輸入法程序檢測到新的Configuration時,它會運行更新操作,然后發現已經有外接設備就會隱藏自己。這樣輸入法就不見了。

詳細邏輯例如以下:

 

    
    //系統端 :WindowManagerService.java
    boolean computeScreenConfigurationLocked(Configuration config, boolean forceRotate) {
            final InputDevice[] devices = mInputManager.getInputDevices();
            final int len = devices.length;
            for (int i = 0; i < len; i++) {
                InputDevice device = devices[i];
                if (!device.isVirtual()) {
                    final int sources = device.getSources();
                    final int presenceFlag = device.isExternal() ?
                            WindowManagerPolicy.PRESENCE_EXTERNAL :
                                    WindowManagerPolicy.PRESENCE_INTERNAL;

                    if (device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
                        //檢測到外接鍵盤
                        config.keyboard = Configuration.KEYBOARD_QWERTY;
                        keyboardPresence |= presenceFlag;
                    }
                }
            }

            // Determine whether a hard keyboard is available and enabled.
            boolean hardKeyboardAvailable = config.keyboard != Configuration.KEYBOARD_NOKEYS;
            if (hardKeyboardAvailable != mHardKeyboardAvailable) {
                mHardKeyboardAvailable = hardKeyboardAvailable;
                mHardKeyboardEnabled = hardKeyboardAvailable;
                mH.removeMessages(H.REPORT_HARD_KEYBOARD_STATUS_CHANGE);
                mH.sendEmptyMessage(H.REPORT_HARD_KEYBOARD_STATUS_CHANGE);
            }
            if (!mHardKeyboardEnabled) {
                config.keyboard = Configuration.KEYBOARD_NOKEYS;
            }
        }
        return true;
    }

    //輸入法端: InputMethodService.java
    @Override public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        
        if (visible) {
            if (showingInput) {
                // onShowInputRequested就會影響輸入法的顯示
                //當有外接鍵盤時,它會返回false
                if (onShowInputRequested(showFlags, true)) {
                    showWindow(true);
                } else {
                    doHideWindow();
                }
            }
            // onEvaluateInputViewShown也會影響輸入法的顯示
            //當有外接鍵盤時,它會返回false
            boolean showing = onEvaluateInputViewShown();
            mImm.setImeWindowStatus(mToken, IME_ACTIVE | (showing ? 
IME_VISIBLE : 0), mBackDisposition);
        }
    }
    

   public boolean onEvaluateInputViewShown() {
        Configuration config = getResources().getConfiguration();
        //檢測Configuration是否標示了有外接鍵盤
        return config.keyboard == Configuration.KEYBOARD_NOKEYS
                || config.hardKeyboardHidden ==
             Configuration.HARDKEYBOARDHIDDEN_YES;
    }

    public boolean onShowInputRequested(int flags, boolean configChange) {
        if (!onEvaluateInputViewShown()) {
            return false;
        }
        if ((flags&InputMethod.SHOW_EXPLICIT) == 0) {
            Configuration config = getResources().getConfiguration();
            //檢測Configuration是否標示了有外接鍵盤
            if (config.keyboard != Configuration.KEYBOARD_NOKEYS) {
                return false;
            }
        }
        if ((flags&InputMethod.SHOW_FORCED) != 0) {
            mShowInputForced = true;
        }
        return true;
    }


輸入法沒法獲得按鍵事件

      

         我們知道,假設要想輸入法通過外接鍵盤輸出中文,它肯定須要從外接鍵盤讀取到英文輸入。而在Android系統中,按鍵等key事件僅僅發送給焦點程序,可是輸入法本身沒法獲得焦點,因此它自然就沒法讀取到外接鍵盤的輸入。


問題的解決

 

讓輸入法和外接鍵盤共存

         從上面的分析可知。輸入法和外接鍵盤沒法共存的根本原因是,輸入法會讀取configuration里的鍵盤屬性值。

解決問題有兩個方法:

1)  改動用到Configuration的相關函數,比方onEvaluateInputViewShown ,onShowInputRequested函數的實現

這種方法看起來可行,可是不行。由於非常多地方可能用到了這個Configuration,改動量比較大,且非常多函數並不是protected或者public,子類是沒法直接改動的。

2)  改動輸入法的Configuration的值

這種方法可行。從源頭上攻克了這個問題,這樣InputMethodService覺得系統沒有外接鍵盤。自然就不會隱藏輸入法了。

  方法2詳細實現例如以下:

         在輸入法初始化和更新Configuration的點主動改動輸入法的Configuration。


         

public class RemoteInputMethod extends InputMethodService { 
   @Override 
   public void onCreate() {
    super.onCreate();
    	updateResources();
   }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        updateResources();
    }

	public void updateResources() {
		Configuration config = new Configuration(getResources().getConfiguration());
        //改動Configuration,讓輸入法覺得系統中沒有外接鍵盤
		config.keyboard = Configuration.KEYBOARD_NOKEYS;
		getResources().updateConfiguration(config, getResources().getDisplayMetrics());
	}
}

讓輸入法獲取外接鍵盤輸入


        輸入法實現輸入有兩部分。一是獲取按鍵事件。二是獲取輸入目標


獲取按鍵事件


       上面已經提到過。輸入法window是沒法獲取外接鍵盤事件的。怎么辦?非常好辦,讓輸入法service創建另外一個普通的window(本文稱作bridge window),並將這個window標示為可接受key事件的window,當它是最top的可接受key事件的window時, 它就能夠獲得焦點並獲得外接鍵盤的輸入。

這樣,它作為中間橋梁就能將外接鍵盤事件傳給輸入法 (同一程序里,非常好做的)。輸入法然后進行翻譯,比方拼音轉為中文。


獲取並更新輸入目標


        輸入法的輸入目標是textView的通信接口InputConnection。它是在程序獲得焦點時候或焦點程序中的焦點view發生變化的時候。焦點程序傳遞給輸入法的。

        所以,問題來了?一旦上面的bridge window獲得焦點后,輸入法的輸入目標就跟着更新了,變成了bridge window的view的InputConnection。這樣即使輸入法完畢了英文到中文的轉換,最后也僅僅能將中文發送給bridge window,並不能發送給用戶想輸入的程序。怎么解?還好Android系統有一個特殊window flag-----WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,當一個window設置了這個flag, 它成為焦點時。輸入法並不會將輸入目標切換為當前焦點window的InputConnection,而是仍舊保持原來的InputConnection。這為我們帶來了希望,也就是說,我們僅僅需將我們的bridge window加入這個flag就可以,其實確實如此。

        可是還存在一個問題。我們知道InputConnection是相應textView的一個通信接口,當用戶改變輸入view時,輸入法中的InputConnection是須要改動的,可是如今因為目標程序已經不是焦點程序了,當用戶觸摸目標程序其它textView導致輸入view改變時,系統並不會通知輸入法去更新InputConnection,這樣一來,輸入法的中文始終僅僅能傳遞給一個textView了。

又怎么解呢?靈光一動,繼續解。當用戶觸摸時。我們能夠讓bridge window臨時失去焦點,這樣目標程序就又一次獲取了焦點,然后輸入view切換時,輸入法就能得到通知,也就是能又一次獲取到新的textView的InputConnection。然后。bridge window又一次獲取焦點,也就是非常短時間后它繼續能夠接受外接鍵盤的輸入了。

     這個方法的重點在bridge window的實現:實現的重點有兩個:

1)     加入WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM flag

2)  監聽OUT_SIDE事件,這樣,當用戶單擊目標程序。切換焦點view時,bridge window可以提前獲知,然后釋放焦點,

   讓目標程序成為焦點,然后完畢焦點view的切換,進而完畢輸入法中的輸入目標InputConnection的更新。

   public class BridgeWindow extends Dialog {
	private static final boolean DEBUG = false;
	private static final String TAG = "MDialog";

	private static final int flagsNask = WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 
    		| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
    		| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
    		| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
	
	private static final int flags = WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 
    		| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
    		| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
	private static final int flags_nofocus = WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM 
    		| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
    		| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

	
	private Window mWindow = null;
	private Handler mHandler = new Handler();
	private MInputMethod mAttachedInputMethod = null;

	public BridgeWindow (Context context) {
		super(context);
		// TODO Auto-generated constructor stub
		init();
	}
	
	public void setAttachedInputMethod(MInputMethod inputMethod) {
		mAttachedInputMethod = inputMethod;
	}

	View mRootView = null;
	public void setContentView(View view) {
		super.setContentView(view);
		mRootView = view;
	}
	
    private void init() {
		// TODO Auto-generated method stub
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setTitle("HardInputMethod");
    	mWindow = this.getWindow();
        LayoutParams lp = mWindow.getAttributes();
        lp.gravity = Gravity.LEFT|Gravity.TOP;
        lp.x = 0;
        lp.y = 0;
    	mWindow.setType(WindowManager.LayoutParams.TYPE_PHONE);
        //初始化window的flag
    	mWindow.setFlags(flags, flagsNask);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
            //檢測到用戶觸摸了bridge window外的區域,那么焦點view可能要發生
            //變化了,輸入法的InputConnection須要更新了。所以在此臨時取消自己
            //的focus
        	if (DEBUG) Log.d(TAG, "release focus");
        	releaseFocus();
        }
        return super.onTouchEvent(event);
    }
	
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
    	if (DEBUG) Log.d(TAG, "onKeyDown" + keyCode);
        //將事件傳遞給輸入法
        mAttachedInputMethod.onKeyDown(keyCode,  event);
        return super.onKeyDown(keyCode, event);
    }
    
	protected void releaseFocus() {
		// TODO Auto-generated method stub
               //將自己配置成不可獲取焦點來讓自己失去焦點
		mWindow.setFlags(flags_nofocus, flagsNask);
		mHandler.removeCallbacks(mFocusRunnable);
               //1s鍾后。讓自己又一次獲取焦點
		mHandler.postDelayed(mFocusRunnable, 1000);
	}
	
	Runnable mFocusRunnable = new Runnable() {
		@Override
		public void run() {
		// TODO Auto-generated method stub
			mWindow.setFlags(flags, flagsNask);
		}
	};
	
	Point mDownPosition = new Point();
	public void onDown(int x, int y) {
		// TODO Auto-generated method stub
		int[] loc = new int[2];
		mRootView.getLocationOnScreen(loc);
		mDownPosition.x = loc[0];
		mDownPosition.y = loc[1] - 50;
		if (DEBUG) Log.d(TAG, "on down position x:" + loc[0] + " y:" + loc[1]);
	}

	public void onMove(int offsetX, int offsetY) {
		// TODO Auto-generated method stub
		updatePositioin(mDownPosition.x + offsetX, mDownPosition.y + offsetY);
	}
	
	private void updatePositioin(int x, int y) {
		LayoutParams lp = mWindow.getAttributes();
            lp.x = x;
            lp.y = y;
            mWindow.setAttributes(lp);
	}
}


完美解決方式


        上面的解決方式是直接在輸入法程序內部改動達到實現外接鍵盤輸入中文。屬於應用程范疇。可是仍有一些問題,而這些問題在程序端是沒法解決的。

那該怎么完美解決呢。Andorid后來的版本號已經攻克了這個。是怎樣解決的?

即全部的按鍵事件先發送給程序。然后程序端的代碼會先將key發送給輸入法,即讓輸入法有一個翻譯轉換過程的機會,然后輸入法再將轉化過的key或者字符發送回程序,也就是說key事件繞了一圈。最后再讓程序端處理。

 

附錄


        近期工作比較忙。代碼還沒有整理好,等整理好后,我會將源代碼發出來。大家能夠一起學習。


/********************************

* 本文來自博客  “愛踢門”

* 轉載請標明出處:http://blog.csdn.net/itleaks

******************************************/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM