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

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

Android输入法扩展之外接键盘中文输入,布布扣,bubuko.com

时间: 2024-08-25 09:50:37

Android输入法扩展之外接键盘中文输入的相关文章

Android输入法扩展之远程输入法

近年来,互联网电视開始火热.乐视TV,小米TV,近期爱奇艺也在大肆的招人做爱奇艺电视.当然还有更被关注的苹果电视.事实上.这个趋势非常正常,也非常合理.传统单纯的接收电视节目的电视已经太传统了,是该被革命了.乐视为代表的新一代互联网电视採取互联网的营销方式,不须要实体店,不须要实体工厂,仅仅需方案.服务,网上预约,直接快递等方式大大减少了成本,同一时候也能够控制库存,预防风险.同一时候他们都坚持硬件不赚钱,服务收费,更看重电视用户对象这一潜在价值. 用户多了,干啥都方便,当然还有更大的智能家居大

iOS 模拟器键盘弹出以及中文输入

1.虚拟键盘的弹出与收起切换: 快捷键:command+shift+K 2.中文输入: Xcode 菜单项 --> Product --> Scheme --> Edit Scheme -->  Run --> Option --> Application Region 设置为 『中国』 重新运行程序,如下图所示就可以切换输入法了

android开发(45) 自定义软键盘(输入法)

概述 在项目开发中遇到一个需求,”只要数字键盘的输入,仅仅有大写字母的输入,某些输入法总是会提示更新,弹出广告等“,使得我们需要自定义输入. 关联到的知识 KeyboardView      一个视图对象,展示了键盘.它需要关联到一个 Keyboard对象才能展示. Keyboard              键盘对象,通过加载xml的配置获得键盘的排列. xml 文件键盘描述     一个xml文件,放置在 xml 资源文件夹下,描述了 显示的键盘按钮,和排列,键盘宽度和高度等. 具体实现 准

给mac 配置外接键盘 和鼠标 输入法切换

作为代码狗,mac的键盘不能敲出幸福的码农生活. 纠结了好久,中途尝试了其他方法,最终还是觉得用mac方便,因为论文和编码都能解决,还能回家加班,比用台式机方便很多. 不能再让我的mac只用于娱乐啦.开始苦逼的两年冲刺!!!! 外接屏幕非常棒,再也不想看小小的13寸啦. 外接键盘+鼠标 问题一: 鼠标反应迟缓. 首先,考虑是否必须要用mm鼠标(apple的), google发现,大家都不推荐.那么,大家是如何解决鼠标迟缓的呢? 方案一:调节鼠标的跟踪速度(通过系统设置,鼠标,请google 如何

win10里如何在中文输入法里添加美国键盘

在控制面板打开“时钟.语言和区域”设置界面,选中“语言”设置   “语言”设置里点击“添加语言”   在添加语言设置里选择“英语”,并点击“打开”按钮,在“区域变量”设置页面里选择“英语(美国)” ,并点击“添加”按钮.   在“语言”设置页面选择“中文”,点击“下移”   在WIN搜索框里输入regedit并按回车键,打开注册表编辑器   在HKEY_CURRENT_USER\Keyboard Layout\Preload,修改“1”的键值为“00000804”   在Substitutes项

android 输入法,里面还集成语音输入

<?xml version="1.0" encoding="utf-8"?> <com.example.android.softkeyboard.LatinKeyboardView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/keyboard" android:layout_alignParentBot

输入法切换小程序(js模拟键盘按键输入成功版)

花了半天时间写了个小程序,基本满足最初需求(但目前只支持IE浏览器). 需求: 在输入汉字的时候没有切换出中文输入法而导致输入了拼音,我们要做的是:将输入的拼音删除并且切换出中文输入法,重新自动在输入法中打出相应的字母. 我的电脑上默认只有两个输入法,中文输入法和美式键盘,所以模拟出”ctrl+空格键“即可在两个输入法之间进行切换. 做的过程和想法: 最初想的是使用网页配合js来做,毕竟界面好做一些.首先想到的就是用js来模拟键盘的输入,想法很好,但是却很难,主要是因为浏览器为了考虑安全性,所以

intellij idea 12、13 win8 下 中文输入覆盖的问题(搜狗输入法或者其他输入法)

最近升级到idea12,发现中文输入存在问题,输入中文的时候会出现空格,并且覆盖后面的字符,这个问题让我很郁闷. 假设idea的安装位置为:D:\Program Files\JetBrains\IntelliJ IDEA 12.1.2 64位模式下: 1.下载JDK 6 2.安装JDK 6,安装JDK 6 ,假设安装位置为:D:\Program Files\JetBrains\IntelliJ IDEA 12.1.2\jdk64 ,也可以采用其他位置,不需要安装jre. 3.设置环境变量:IDE

android安卓屏蔽禁用系统输入法,自定义软键盘,解决EditText光标问题demo

目前很多的输入法都有自动提示补全功能,在一些应用场景里不适用,需要禁用系统输入法,自定义软键盘,EditText的光标问题是比较头疼的,网上的说法很多,然而大部分都是解决不了问题的.以下是本人做的一个demo供网友参考. 直接上代码: xml软键盘:         <android.inputmethodservice.KeyboardView             android:id="@+id/keyboard_view"             android:lay