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

        你們想不想要這樣一臺Android  Surface平板,看着就過癮吧。java

                

     

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

那到底何時需要用到外接鍵盤呢?本人認爲首先要知足例如如下兩個條件。android

1)   平板和外接鍵盤完美融合,組合後很是像筆記本使用模式。相似上面Android Surface的機器,平板和鍵盤經過磁性本身主動粘合,變身筆記本模式ubuntu

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

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

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

      那爲啥這樣的Android超極本就不夠火呢?固然有很是多緣由啊,比方平板自己需求量小,Android自己就不適合辦公。固然確定也有另一個小緣由。它這個物理鍵盤竟然不能中文輸入。學習

所以,Android平板要進入辦公領域並流行,需要實現相似PC端中文輸入的體驗。this

     本文說到的外接鍵盤中文輸入,重在中文兩字。其實,Android自己是支持外接鍵盤的。但是僅僅能夠實現英文輸入。其實。咱們在前幾篇文章已經說到了輸入法,也已經分析到,Android要想輸入中文,必須經過輸入法。lua

那爲啥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

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

相關文章
相關標籤/搜索