Android快捷键切换输入法

Android快捷键切换输入法

做为一个桌面环境,输入法是必不可少的,输入法的切换也应该按照桌面的使用方式,control + shift 或者 control + space 进行切换,对于一个安卓桌面,所以需要对其输入法的切换进行改造,笔者暂时只实现了快捷键切换系统输入法,然而实际上,安卓的输入法与桌面使用方式还有一些不同之处,包括配置,键盘,提示词等等。

首先,从系统角度来认识一下安卓的输入法框架,他包含哪些内容,各模块之间又是什么关系,有了基本认识以后,再对比需求,尝试功能实现方式。

输入法管理服务的整体框架

输入法框架包含以下部分:

InputMethodManagerService

输入法系统服务(InputMethodManagerService),简称IMMS,由SystemServer启动,所以也是运行在system_server进程。MultiClientInputMethodManagerService是多会话输入法管理服务,主要应用在多屏设备上,支持每个会话使用不同的输入法功能。

源码位于 frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java

系统输入法的主要逻辑全都在这个类里面实现,包含了输入法的所有管理功能:

com.android.server.inputmethod.InputMethodManagerService#setInputMethodLocked //设置输入法

com.android.server.inputmethod.InputMethodManagerService#switchToNextInputMethod //切换输入法

com.android.server.inputmethod.InputMethodManagerService#showInputMethodMenu //显示输入法菜单

com.android.server.inputmethod.InputMethodManagerService#onShellCommand //响应shell命令

...

InputMethodService

输入法服务(InputMethodService),简称IMS,三方输入法要继承实现这个类,当你要自己开发一个输入法的时候,就是通过继承这个service,注册到系统,提供其他应用使用,具体可参考官方文档。他是由IMMS启动,启动函数是startInputOrWindowGainedFocus,这个主要由InputMethodManager控制。

源码位于 frameworks/base/core/java/android/inputmethodservice/InputMethodService.java

输入法服务是输入法的具体实现,包含了每个输入法的所有功能:

android.inputmethodservice.InputMethodService#onCreateInputView //输入法键盘view

android.inputmethodservice.InputMethodService#onCreateCandidatesView //提示词view

android.inputmethodservice.InputMethodService#getCurrentInputConnection //处理文本的InputConnection

android.inputmethodservice.InputMethodService#switchInputMethod(java.lang.String, android.view.inputmethod.InputMethodSubtype) //切换输入法,后文会讲解

InputMethodManager

输入法管理器(InputMethodManager),简称IMM,熟悉安卓架构的同学都理解,xxxManager是系统服务暴露给应用端的功能接口,使用系统服务基本功能在这个类里面就可以调用,但是又应该都理解,使用xxxManager限制非常多(也是因为各种hook技术),当你有一个需求的时候他大概率不能满足。APP一般会使用这个类来处理输入法,包含输入法唤起,软键盘,切换弹框等功能:

android.view.inputmethod.InputMethodManager#showInputMethodPicker //输入法切换弹框

android.view.inputmethod.InputMethodManager#showSoftInput(android.view.View, int) //显示软键盘,唤起输入法

对于没有键盘的手机来说,软键盘是必不可少的,showsoftinput就是唤起来输入法,当然安卓也提供了软键盘的控制,怎么显示,显不显示。

以下是显示/隐藏输入法的时序图:

以上只是概述了Android输入法的整体框架功能,具体调用逻辑,实现细节可以从源码中再做研究,或者可以参考这个链接

输入法调试方法

输入法的安装会注册service,会由PackageManager管理,使用状态保存在系统数据库settings(现版本已保存在xml),输入法相关的数据保存在存储位置:/data/system/users/0/settings_secure.xml。

ini 复制代码
enabled_input_methods 已使能输入法
<setting id="1638" name="default_input_method" value="com.android.inputmethod.latin/.LatinIME" package="android" defaultValue="com.android.inputmethod.latin/.LatinIME" defaultSysSet="true" preserve_in_restore="true" />
  
default_input_method  默认输入法
<setting id="1374" name="enabled_input_methods" value="com.android.inputmethod.latin/.LatinIME:com.iflytek.inputmethod/.FlyIME" package="android" defaultValue="com.android.inputmethod.latin/.LatinIME:com.iflytek.inputmethod/.FlyIME" defaultSysSet="true" preserve_in_restore="true" />

enabled_input_methods是在设置里面管理屏幕键盘打开开关,打开之后,才能设置为default_input_method,而default_input_method才是真正使用的输入法。

可以通adb命令设置使能输入法和默认输入法

查询使能的输入法

bash 复制代码
C:\Users\huyan> adb shell ime list -s
com.android.inputmethod.latin/.LatinIME
all.one.test/.AndroidInputMethodService
com.iflytek.inputmethod/.FlyIME
bash 复制代码
C:\Users\huyan> adb shell settings get secure enabled_input_methods
com.android.inputmethod.latin/.LatinIME:all.one.test/.AndroidInputMethodService:com.iflytek.inputmethod/.FlyIME

设置默认输入法

arduino 复制代码
adb shell settings put secure default_input_method com.iflytek.inputmethod/.FlyIME

查询默认输入法

vbnet 复制代码
C:\Users\huyan> adb shell settings get secure default_input_method
com.iflytek.inputmethod/.FlyIME

这些命令的执行逻辑在上面说到的com.android.server.inputmethod.InputMethodManagerService#onShellCommand,如果在应用进程也是不能直接使用的。

快捷键切换输入法实现

输入法的切换其实有两个范畴,InputMethodInfo和InputMethodSubtype,InputMethodInfo对应的是每一个IMS,即一个输入法,InputMethodSubtype是输入法的子类型,比如不同语言,不同输入type,输入法本身做切换是切换子类型,本文实现的是切换整个输入法。

应用进程输入法切换的两种方式

一种是通过android.view.inputmethod.InputMethodManager#showInputMethodPicker,显示系统弹框给用户确认,这部分逻辑在com.android.server.inputmethod.InputMethodManagerService#showInputMethodMenu。输入法的确认逻辑在com.android.server.inputmethod.InputMethodManagerService#setInputMethodLocked。也就是说如果你是一个安卓应用,你想要切换输入法,必须弹窗用户确认来选择。

另一种是在IMM和IMS都提供了切换输入法的接口,先看IMM的switchToNextInputMethod函数如下,此函数已经废弃,InputMethodService#switchToNextInputMethod来替代,先不管,尝试一下,确实已经不能生效。

typescript 复制代码
/**
 * Force switch to the next input method and subtype. If there is no IME enabled except
 * current IME and subtype, do nothing.
 * @param imeToken Supplies the identifying token given to an input method when it was started,
 * which allows it to perform this operation on itself.
 * @param onlyCurrentIme if true, the framework will find the next subtype which
 * belongs to the current IME
 * @return true if the current input method and subtype was successfully switched to the next
 * input method and subtype.
 * @deprecated Use {@link InputMethodService#switchToNextInputMethod(boolean)} instead. This
 * method was intended for IME developers who should be accessing APIs through the service.
 * APIs in this class are intended for app developers interacting with the IME.
 */
@Deprecated
public boolean switchToNextInputMethod(IBinder imeToken, boolean onlyCurrentIme) {
    return InputMethodPrivilegedOperationsRegistry.get(imeToken)
            .switchToNextInputMethod(onlyCurrentIme);
}

此函数的实现是com.android.internal.inputmethod.InputMethodPrivilegedOperations#switchToNextInputMethod

java 复制代码
public boolean switchToNextInputMethod(boolean onlyCurrentIme) {
    final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
    if (ops == null) {
        return false;
    }
    try {
        return ops.switchToNextInputMethod(onlyCurrentIme);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

IInputMethodPrivilegedOperations是Aidl生成的IMMS的远程接口,ops如果为null,那功能就不生效。如果不能提供有效的imeToken,就无法调用这个接口。

scss 复制代码
InputMethodPrivilegedOperationsRegistry.get(imeToken)
        .switchToNextInputMethod(onlyCurrentIme);

再看IMS的switchInputMethod函数,这个是有效的,即提供给IME开发者使用这个API,因为输入法本身就能拿到imeToken。

typescript 复制代码
    /**
     * Force switch to a new input method, as identified by {@code id}.  This
     * input method will be destroyed, and the requested one started on the
     * current input field.
     *
     * @param id Unique identifier of the new input method to start.
     * @param subtype The new subtype of the new input method to be switched to.
     */
    public final void switchInputMethod(String id, InputMethodSubtype subtype) {
        mPrivOps.setInputMethodAndSubtype(id, subtype);
    }

在应用进程如果要用API来切换输入法,就只能在输入法内部来调用,那样系统就凭空多出来一个输入法,而且切换完了之后,本身的IMS就退出了,没法继续进行切换。

当然,Setting应用做为系统设置应用也提供了修改输入法的接口,可以参考这个链接

系统进程切换输入法

本文在IMMS来实现切换,并没有修改IMMS对外的接口,新增的逻辑也尽量不要修改现有逻辑。

首先系统快捷键在系统快捷拦截函数com.android.server.policy.PhoneWindowManager#interceptKeyBeforeDispatching,找到现在Android系统已有的处理切换的逻辑。但是他这个切换只是换了键盘布局并没有,切换输入法,这也是Android切换输入法操作和桌面操作的不同。

ini 复制代码
        // Handle keyboard language switching.
        final boolean isCtrlOrMetaSpace = keyCode == KeyEvent.KEYCODE_SPACE
                && (metaState & (KeyEvent.META_CTRL_MASK | KeyEvent.META_META_MASK)) != 0;
        if (down && repeatCount == 0
                && (keyCode == KeyEvent.KEYCODE_LANGUAGE_SWITCH || isCtrlOrMetaSpace)) {
            	int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
            	mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction);
            return -1;
        }

改为:

ini 复制代码
        // Handle keyboard language switching.
        final boolean isCtrlOrMetaSpace = keyCode == KeyEvent.KEYCODE_SPACE
                && (metaState & (KeyEvent.META_CTRL_MASK | KeyEvent.META_META_MASK)) != 0;
        if (down && repeatCount == 0
                && (keyCode == KeyEvent.KEYCODE_LANGUAGE_SWITCH || isCtrlOrMetaSpace)) {
            int direction = (metaState & KeyEvent.META_SHIFT_MASK) != 0 ? -1 : 1;
            Slog.w(TAG, "direction:"+direction  + "  deviceid:" +  event.getDeviceId());
            mWindowManagerFuncs.switchKeyboardLayout(event.getDeviceId(), direction);

            if (mInputMethodManagerInternal == null) {
                mInputMethodManagerInternal =  LocalServices.getService(InputMethodManagerInternal.class);
            }
            mInputMethodManagerInternal.switchToNextInputMethod(false);
            return -1;
        }

InputMethodManagerInternal 是IMMS的代理类,需要在IMMS里面做真正的实现。

主要修改了以下函数:

frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java

java 复制代码
@BinderThread
    private boolean switchToNextInputMethod(boolean onlyCurrentIme) {
        synchronized (mMethodMap) {
            final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
                    onlyCurrentIme, mMethodMap.get(mCurMethodId), mCurrentSubtype);
            if (nextSubtype == null) {
                return false;
            }
			//全局修改,不需要传入token
            setInputMethodWithSubtypeIdLocked(null, nextSubtype.mImi.getId(),
                    nextSubtype.mSubtypeId);
            return true;
        }
    }

services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java

ini 复制代码
分成了两个list,需要交叉遍历
mSwitchingAwareRotationList	 
mSwitchingUnawareRotationList	

       public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme, InputMethodInfo imi,
                InputMethodSubtype subtype) {
            if (imi == null) {
                return null;
            }
//            if (imi.supportsSwitchingToNextInputMethod() && nextIndex <= mSwitchingAwareRotationList.mImeSubtypeList.size() - 1) {
//                return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
//                        subtype);
//            } else {
//                return mSwitchingUnawareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
//                        subtype);
//            }
            
            if ( nextIndex <= mSwitchingAwareRotationList.mImeSubtypeList.size() - 1) {
                nextIndex++;
                return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
                        subtype);
            } else {
                if(nextIndex == mSwitchingAwareRotationList.mImeSubtypeList.size() + mSwitchingUnawareRotationList.mImeSubtypeList.size() - 1){
                    nextIndex = 0;
                } else {
                    nextIndex++;
                }
                ImeSubtypeListItem itmes = mSwitchingUnawareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
                        subtype);
                return itmes;
            }
        }

supportsSwitchingToNextInputMethod是从每个输入法的属性取出来的,这里全局切换不能使用这个逻辑。

ini 复制代码
            supportsSwitchingToNextInputMethod = sa.getBoolean(
                    com.android.internal.R.styleable.InputMethod_supportsSwitchingToNextInputMethod,

com.android.server.inputmethod.InputMethodSubtypeSwitchingController.DynamicRotationList#getNextInputMethodLocked

ini 复制代码
public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
                InputMethodInfo imi, InputMethodSubtype subtype) {
            int currentUsageRank = getUsageRank(imi, subtype);
            if (currentUsageRank < 0) {
                // if (DEBUG) {
                //     Slog.d(TAG, "IME/subtype is not found: " + imi.getId() + ", " + subtype);
                // }
                // return null;
                //遍历一遍了,又从rank 下标0开始
                currentUsageRank = 0;
            }
            final int N = mUsageHistoryOfSubtypeListItemIndex.length;
            //如果只有一个subtype就直接返回
            if( N == 1){
                return mImeSubtypeList.get(0);
            }

            for (int i = 1; i < N; i++) {
                final int subtypeListItemRank = (currentUsageRank + i) % N;
                final int subtypeListItemIndex =
                        mUsageHistoryOfSubtypeListItemIndex[subtypeListItemRank];
                final ImeSubtypeListItem subtypeListItem =
                        mImeSubtypeList.get(subtypeListItemIndex);
                if (onlyCurrentIme && !imi.equals(subtypeListItem.mImi)) {
                    continue;
                }
                return subtypeListItem;
            }
            return null;
        }

com.android.server.inputmethod.InputMethodSubtypeSwitchingController#getNextInputMethodLocked

kotlin 复制代码
        public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
                InputMethodInfo imi, InputMethodSubtype subtype) {
            if (imi == null) {
                return null;
            }
            //如果只有一个subtype就直接返回
            if (mImeSubtypeList.size() <= 1) {
                return mImeSubtypeList.get(0);
            }
            final int currentIndex = getIndex(imi, subtype);
            if (currentIndex < 0) {
                return null;
            }
            final int N = mImeSubtypeList.size();
            for (int offset = 1; offset < N; ++offset) {
                // Start searching the next IME/subtype from the next of the current index.
                final int candidateIndex = (currentIndex + offset) % N;
                final ImeSubtypeListItem candidate = mImeSubtypeList.get(candidateIndex);
                // Skip if searching inside the current IME only, but the candidate is not
                // the current IME.
                if (onlyCurrentIme && !imi.equals(candidate.mImi)) {
                    continue;
                }
                return candidate;
            }
            return null;
        }

此方法切换输入法的调用栈是:

  1. com.android.server.policy.PhoneWindowManager#interceptKeyBeforeDispatching
  2. com.android.server.inputmethod.InputMethodManagerInternal#switchToNextInputMethod
  3. com.android.server.inputmethod.InputMethodManagerService.LocalServiceImpl#switchToNextInputMethod
  4. com.android.server.inputmethod.InputMethodManagerService#switchToNextInputMethod
  5. com.android.server.inputmethod.InputMethodManagerService#setInputMethodWithSubtypeIdLocked
  6. com.android.server.inputmethod.InputMethodManagerService#setInputMethodLocked
arduino 复制代码
void setInputMethodLocked(String id, int subtypeId)

只需要id,即可完成切换。至此,即完成了组合键control + space 实现切换系统输入法。

相关推荐
inmK11 小时前
蓝奏云官方版不好用?蓝云最后一版实测:轻量化 + 不限速(避更新坑) 蓝云、蓝奏云第三方安卓版、蓝云最后一版、蓝奏云无广告管理工具、安卓网盘轻量化 APP
android·工具·网盘工具
giaoho2 小时前
Android 热点开发的相关api总结
android
咖啡の猫3 小时前
Android开发-常用布局
android·gitee
程序员老刘4 小时前
Google突然“变脸“,2026年要给全球开发者上“紧箍咒“?
android·flutter·客户端
Tans54 小时前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
雨白4 小时前
实现双向滑动的 ScalableImageView(下)
android
峥嵘life4 小时前
Android Studio新版本编译release版本apk实现
android·ide·android studio
studyForMokey7 小时前
【Android 消息机制】Handler
android
敲代码的鱼哇7 小时前
跳转原生系统设置插件 支持安卓/iOS/鸿蒙UTS组件
android·ios·harmonyos
翻滚丷大头鱼7 小时前
android View详解—动画
android