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

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

原文地址:https://www.cnblogs.com/mfrbuaa/p/5114169.html