查看原文
其他

基于Android输入法开发,制作一个微信斗图APP

刘望舒 2022-06-30

The following article is from 搜狐技术产品 Author 小学生°


刘望舒

读完需要

20分钟

速读仅需12分钟


作者:小学生°

来源:搜狐技术产品


01

导读


微信斗图的应用有很多,但大部分都是通过微信分享来实现的,需下载 APP,下载表情并分享到微信联系人,操作步骤复杂。而基于输入法的微信斗图就少了不少操作,现在市面上的输入法大都有斗图模块,然而有些强迫症患者,对第三方输入法的斗图模块设计并不满意,或者操作步骤依然复杂、有捆绑模块、不喜欢输入法有广告、需要读取隐私信息等各种原因,就是不想用不喜欢的第三方输入法,基于这个需求,可以把斗图模块单独抽出来,制作一个专注于斗图的输入法 APP。

通过分析市面上已有的输入法斗图模块可以得知微信的一个隐藏功能,就是在聊天输入框输入类似/storage/emulated/0/Android/data/cache/a.gif的图片文件路径时,微信会自动解析图片,并弹出是否发送表情的确认弹窗,点击确认就直接发送了图片,如果是 gif 动图则直接转换成表情。所以我们只需在输入法面板中通过关键字搜索表情后展示表情列表,直接点击表情上屏图片路径,即可实现自动发送。那么问题的关键就在于如何构建一个输入法项目,最后为了操作更方便,可以使用辅助功能提升用户体验。


02

输入法开发


1.API简介


简单来说,开发一个输入法,只需要用到一个核心类和几个可有可无的辅助类。

核心类是InputMethodService,一个输入法几乎所有的功能都是由它来实现的,包括键盘界面的搭建、键盘语言的切换、拼音汉字的转换、候选词的展示、文字的上屏等各种逻辑都通过这个类来实现。InputMethodService类有如下几个主要方法来管理输入法服务的生命周期:

  • onCreate(): 输入法开始创建,内部已经实现设置 theme、创建 window、填充 root view、设置布局方式等,我们也可以在此处做一些初始化操作,但一定不要忘了调用 supper.onCreate();

  • onCreateInputView(): 返回一个 view 作为输入法的键盘布局,通常这个view 是由KeyboardView 和 Keyboard 两个辅助类生成,当然也完全可以自定义。切换一次输入法只会调用一次;

  • onCreateCandidatesView(): 返回一个 view 来展示候选词,这个 view 可有可无,会覆盖到应用上方,一般用半透明的背景,但市面上的输入法一般都用来显示拼音部分,而把候选词放入 InputView。同样切换一次输入法只会调用一次;

  • onStartInputView(EditorInfo): 开始输入的时候调用,每次唤起键盘或切换 EditText 都会调用,并把 EditText 的 EditorInfo 传过来,输入法要根据 EditorInfo 的信息判断中英文、数字、回车键类型等,来展示不同的键盘,也就是动态切换 InputView 的布局;

  • onFinishInput(): 输入结束的时候调用,此时可以做一些 reset 操作,比如隐藏 CandidatesView,恢复 InputView 为默认布局等;

  • onDestroy(): 一般在切换其他输入法的时候会被调用,实测输入法不用一段时间,系统也会暂时杀掉进程,此时也会被调用。

可能有同学对上面提到的InputViewCandidatesView到底是什么还有些疑问,下面这张图可以表达地很清楚:



而这些方法定义的生命周期则可以通过Android Developers[1]官方的一张图来深入理解,相信每篇输入法相关的文章都会有这张经典的图:



InputMethodService中,负责与正在输入的APP交互的是其持有的InputConnection对象,可通过getCurrentInputConnection()方法获取,InputConnection有如下几个常用的方法:

  • getTextBeforeCursor(n): 获取光标前 n 个字符;

  • getTextAfterCursor(n): 获取光标后 n 个字符;

  • getSelectedText(): 获取已经被选中的字符;

  • deleteSurroundingText(beforeLength, afterLength): 删除光标前后的字符;

  • commitText(text): 提交 text 到 APP 的输入框;

  • sendKeyEvent(keyEvent): 发送特殊的按键 code,如回车、退格等。

再说说上文中提到的两个辅助类KeyboardViewKeyboard,已经提到,InputView可以是一个KeyboardViewKeyboardView内部已经封装了一些键盘的通用功能,比如特殊按键(回车、退格等)的发送、滑动手势功能、长按键盘功能、按键音的播放等等。而KeyboardView只是一个空的view它的布局是没有确定的,查找它的代码我们发现其中有一个成员Keyboard,这就是另外一个辅助类,Keyboard的主要任务就是承载特定的键盘布局,如中英文键盘、数字键盘、符号键盘等,并且将布局中的按键和按键内容的KeyCode对应起来,通常这种对应关系可以用一个xml文件来定义(如图 2.1.3),在构造Keyboard的时候去加载这个xml文件,这样KeyboardView就持有Keyboard所有按键的引用,在按下某个按键时,通过已设置的OnKeyboardActionListener将按键对应的KeyCode传到InputMethodService(要记得,此时KeyboardView已经是InputMethodService持有的InputView),或者经过处理——比如将拼音处理成汉字——并将处理后的文字发送给InputMethodServiceInputMethodService调用commitText()方法,将文字上屏到当前 APP 聚焦的EditText



对于KeyboardViewKeyboard这两个类的使用,Google官方有一个很好的实例,感兴趣的同学可以下载来研究一下:SoftKeyboard Sample[2]


2. 一般输入法的开发步骤


接下来介绍一下一个输入法完整的开发和配置,当然了我们的微信斗图APP因为不涉及文字的处理和输入,就少了很多步骤,了解了完整的流程,再来做斗图APP就不在话下了。


2.1 新建项目与配置

输入法应用和普通的APP没有什么大的区别,像平常一样新建项目即可,然而要让系统知道这是一个输入法应用,则需要做一些配置。

首先新建一个类SoftInputService继承自InputMethodService,内容可以先留空。然后新建一个SettingsActivity作为输入法的设置界面,同样先不用写内容。接下来就是配置了,在AndroidManifest中如下配置SoftInputService

<service android:name="com.package.InputService" android:permission="android.permission.BIND_INPUT_METHOD"> <intent-filter> <action android:name="android.view.InputMethod" /> </intent-filter> <meta-data android:name="android.view.im" android:resource="@xml/method" /></service>

系统检测到android.view.InputMethod这个action表明本应用是一个输入法,同时检测是否有android.permission.BIND_INPUT_METHOD这个权限。后面的meta-data则是对输入法的配置,其中的@xml/method如下:

<input-method xmlns:android="http://schemas.android.com/apk/res/android" android:settingsActivity="com.package.SettingsActivity" android:supportsSwitchingToNextInputMethod="true"></input-method>显然settingsActivity就是配置输入法的设置界面,安装后在系统设置中的语言和输入法中可以看到我们的输入法,点击后就会进入这个设置界面,如果修改了SettingsActivity的名称或路径,一定要把这里也同步一下,否则系统找不到类就无法跳转。supportsSwitchingToNextInputMethod表示是否想要在我们的输入法内切换输入法。

至此,如果打包安装,我们就能在系统设置中看到我们的输入法,但还没有实际的功能。


2.2 实现键盘布局与文字上屏

前面提到过,可以用KeyboardViewKeyboard来实现简单的键盘布局,所以SoftInputService中的onCreateInputView()方法可以返回一个KeyboardView对象,在返回前要设置好Keyboard以及监听:

@Overridepublic View onCreateInputView() { mKeyboard = new Keyboard(getApplicationContext(), R.xml.qwerty); mInputView = new KeyboardView(getApplicationContext(), null); mInputView.setOnKeyboardActionListener(this); mInputView.setKeyboard(mKeyboard); return mInputView;}

Keyboard承载着键盘按键和KeyCode的对应关系,通过构造方法填充xml文件来实现,以下是一个完整的QWERTY键盘的xml映射:

<?xml version="1.0" encoding="utf-8"?><Keyboard xmlns:android="http://schemas.android.com/apk/res/android" android:keyWidth="10%p" android:horizontalGap="0px" android:verticalGap="0px" android:keyHeight="@dimen/key_height"> <Row> <Key android:codes="113" android:keyLabel="q" android:keyEdgeFlags="left"/> <Key android:codes="119" android:keyLabel="w"/> <Key android:codes="101" android:keyLabel="e"/> <Key android:codes="114" android:keyLabel="r"/> <Key android:codes="116" android:keyLabel="t"/> <Key android:codes="121" android:keyLabel="y"/> <Key android:codes="117" android:keyLabel="u"/> <Key android:codes="105" android:keyLabel="i"/> <Key android:codes="111" android:keyLabel="o"/> <Key android:codes="112" android:keyLabel="p" android:keyEdgeFlags="right"/> </Row> <Row> <Key android:codes="97" android:keyLabel="a" android:horizontalGap="5%p" android:keyEdgeFlags="left"/> <Key android:codes="115" android:keyLabel="s"/> <Key android:codes="100" android:keyLabel="d"/> <Key android:codes="102" android:keyLabel="f"/> <Key android:codes="103" android:keyLabel="g"/> <Key android:codes="104" android:keyLabel="h"/> <Key android:codes="106" android:keyLabel="j"/> <Key android:codes="107" android:keyLabel="k"/> <Key android:codes="108" android:keyLabel="l" android:keyEdgeFlags="right"/> </Row> <Row> <Key android:codes="-1" android:keyIcon="@drawable/sym_keyboard_shift" android:keyWidth="15%p" android:isModifier="true" android:isSticky="true" android:keyEdgeFlags="left"/> <Key android:codes="122" android:keyLabel="z"/> <Key android:codes="120" android:keyLabel="x"/> <Key android:codes="99" android:keyLabel="c"/> <Key android:codes="118" android:keyLabel="v"/> <Key android:codes="98" android:keyLabel="b"/> <Key android:codes="110" android:keyLabel="n"/> <Key android:codes="109" android:keyLabel="m"/> <Key android:codes="-5" android:keyIcon="@drawable/sym_keyboard_delete" android:keyWidth="15%p" android:keyEdgeFlags="right" android:isRepeatable="true"/> </Row> <Row android:rowEdgeFlags="bottom"> <Key android:codes="-3" android:keyIcon="@drawable/sym_keyboard_done" android:keyWidth="15%p" android:keyEdgeFlags="left"/> <Key android:codes="-2" android:keyLabel="123" android:keyWidth="10%p"/> <Key android:codes="-101" android:keyIcon="@drawable/sym_keyboard_language_switch" android:keyWidth="10%p"/> <Key android:codes="32" android:keyIcon="@drawable/sym_keyboard_space" android:keyWidth="30%p" android:isRepeatable="true"/> <Key android:codes="46,44" android:keyLabel=". ," android:keyWidth="15%p"/> <Key android:codes="10" android:keyIcon="@drawable/sym_keyboard_return" android:keyWidth="20%p" android:keyEdgeFlags="right"/> </Row></Keyboard>

Keyboard已填充好,那么接下来调用KeyboardView.setKeyboard(Keyboard)方法,把Keyboard传入KeyboardView,此时KeyboardView会收集Keyboard的所有按键的映射关系及所在位置(从xml文件中的keyWidthkeyHeight属性可看出,每个按键的位置已然固定),在点击按键时,KeyboardView调用getKeyIndices(x, y)方法,根据点击屏幕的位置,计算出点击的是哪个按键,再用已设置好的OnKeyboardActionListener把输出的文字回调到SoftInputServiceSoftInputService收到文字就可以调用InputConnection.commitText(text)将文字上屏到正在输入的APP了。至此,一个具有输入英文、数字、符号最简单功能的输入法已经完成。


2.3 复杂功能输入法的实现

当然市面上任何一款输入法APP都不会这么简单,但我们知道了原理,复杂的功能也能一一实现。

首先我们发现用来展示候选词的CandidatesView并没有用上,这个悬浮的view我们可以在输入时,在SoftInputService中调用setCandidatesViewShown(show)方法来动态的显示和隐藏,而展示内容则完全可以自定义,一般也就是展示一个预测输入的文字或词语的列表。

至于怎样将拼音转成可能的汉字,这就需要借助一些词库来实现,各大输入法都有自己的词库,甚至都有自己的云词库,在输入时上传到网络,可以匹配一些网络流行语等。这些操作,都可以封装在KeyboardView中或用单独的计算模块来实现。

而常见的26键键盘和九宫格键盘,则可以通过修改KeyboardView来动态实现,不管是怎样的布局,最终commit出来的文字都是计算处理之后的,而不像上面简单的放在xml里面实现,由于KeyboardView功能上的局限性和外观简单的原因,成熟的输入法几乎都是自己定义InputView而不是使用简单的KeyboardView


03

微信斗图 APP


我们已经知道在微信输入框中输入一个图片路径,就可以直接发送这个图片,那么斗图APP的关键在于获取一个表情列表,展示在键盘布局中,点击图片表情时将图片的本地缓存路径作为文本commit出来。

对于表情来源,我们可以从现有的各大表情网站抓取,比如搜狗表情、斗图啦、斗图终结者等网站,都有大量的表情资源,通过Google Chrome的检查功能或抓包操作可以得知用关键字搜索表情的接口,从而很轻松地获取到这些网站的资源(仅供学习交流使用,切勿商用或恶意抓取)。

表情图片的显示,我们可以用一个简单的GridView来展示,如何获取图片本地缓存的路径,可以查询主流图片加载框架(Glide、Fresco等)的API。

接下来我们会发现一个问题,就是搜索表情的关键词该如何传入。因为我们APP本身就是一个输入法,不可能在输入法内部切换到其他的输入法来输入文字,而我们自己的输入法又不支持输入文字(实现输入汉字着实有些复杂,我们将重点放到斗图上)。

这个问题有多种方法变通来实现,首先就是可以在输入法内开启一个Activity,在Activity内唤出正常的输入法输入文字,关闭时切换回斗图输入法并回调已输入的文字,这种方法的问题就是输入法应用允许切换到其他输入法,但不允许切换回自己的输入法,哪怕自己的在自己的输入法存活的Service中操作,也是无效的。虽然可以借助辅助功能来实现,但输入法来回切换在视觉上的表现并不好。

另一种方法就是在微信输入框内输入好想要搜索的词,再调出斗图输入法来读取这个已经输入的词。输入法读取聚焦的输入框是被允许的,调用方式就是上文提到过的InputConnection.getTextBeforeCursor(n)来实现,取到搜索词后就可以去各表情包网站抓取相关表情并展示,此时我们可以调用InputConnection.deleteSurroundingText()来删掉输入框中的搜索词,因为我们不再需要它,而且要避免手动删除来提升用户体验。

我们可以设置GridViewOnItemClickListener或自定义监听来回调点击表情对应的图片路径,然后调用InputConnection.commitText(text)方法输入到微信,此时微信会自动解析并弹出一个确认对话框(图 3.1),点击发送即可发送图片,gif图则会自动解析成动态的微信表情发送。



04

斗图功能优化


以上的成果基本可以让你在斗图过程中立于不败之地,但对于追求极致体验的同学,还有待优化之处,以下介绍两点可以优化的地方,都需要用到辅助功能的服务。

第一个问题,微信弹出对话框后,还要点击确认,多这一步操作,怎样做到点击表情立即发送呢,这时我们想到了AccessibilityService,通过它可以实现自动点击。由于AccessibilityService不是今天的重点,具体怎么使用同学们可以 Google 一下,下面放上AccessibilityService实现部分的代码:

@Overridepublic void onAccessibilityEvent(AccessibilityEvent event) { CharSequence packageName = event.getPackageName(); AccessibilityNodeInfo root = getRootInActiveWindow(); if (root != null) { if ("com.tencent.mm".equals(packageName.toString())) { // 微信包名 long currentTime = System.currentTimeMillis(); // 获取当前时间 long commitTextTime = SharedPrefUtil.getLong (Constants.KEY_TIMESTAMP_ASSIST, 0); // 获取图片路径提交时间 if (currentTime - commitTextTime < 500) { // 图片路径提交后 500ms 内则触发 List<AccessibilityNodeInfo> confirm = root.findAccessibilityNodeInfosByText("确定"); // 找到确认按钮 if (confirm != null && confirm.size() > 0) { confirm.get(0).performAction(ACTION_CLICK); // 触发点击事件 } } } root.recycle(); }}我们知道AccessibilityService是根据文本来找控件的,而微信中有“确定”二字的控件肯定不止一个,所以我们可以在commit图片路径之后存下一个时间戳,检测控件时获取当前的时间戳,这两个时间戳的间隔在一定范围内(此处定为 500ms),就足以说明这个“确定”就是微信弹出的确定发送图片的按钮。此时就实现了自动发送,不过在使用过程中千万不要手抖,避免发错表情而引起尴尬。

第二个问题,在微信输入框内输入好搜索词之后,我们得下拉通知栏,点击通知栏“选择输入法”,再点击弹出的输入法选择器才能完成切换输入法,需要至少一个滑动和两个点击三步操作,能不能一步到位呢?当然能。同样是利用AccessibilityService来实现:

@Overridepublic void onAccessibilityEvent(AccessibilityEvent event) { CharSequence packageName = event.getPackageName(); if ("com.tencent.mm".equals(packageName.toString())) { if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_LONG_CLICKED) { InputMethodManager inputMethodManager = (InputMethodManager) App.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); if (inputMethodManager != null) { inputMethodManager.showInputMethodPicker(); } } } AccessibilityNodeInfo root = getRootInActiveWindow(); if (root != null) { if ("android".equals(packageName.toString())) { List<AccessibilityNodeInfo> infos = root.findAccessibilityNodeInfosByText("Gifin"); if (infos != null && infos.size() > 0) { infos.get(0).getParent().performAction(ACTION_CLICK); } } root.recycle(); }}代码前半段,检测到在微信内长按操作则弹出输入法选择器,代码后半段,检测到输入法选择器弹出则选择名为“Gifin”的输入法,即我们自己的输入法。这样一来,当我们用正常的输入法输入好搜索词之后,只需长按自己的头像,AccessibilityService会帮我们切换成斗图输入法,并开始搜索展示搜索结果,再点击结果直接发送表情一气呵成。

其他的我们可以自由发挥,比如加入搜索历史,使用多个图源,加入收藏夹,保存到本地等功能。



05

总结


到此处,我们可以说一句“斗图我从来没输过”,这一点也不吹牛,目前市面上斗图最快最便捷的就是输入法类APP,而体验过某输入法后,发现实现一个完整的斗图过程,也需要至少5步操作,现在我们只需要输入搜索词、搜索并展示、点击发送3步就可以完成。

当然了,对于很多同学来说,斗图不是目的,学习技术才是重点,虽然Google上输入法相关的文章一搜一大堆,但能结合实际体验的并不多,也许这个APP的技术含量在某些大牛看来并不高,但对于初学者来说,或许能在众多教程中找到一个更容易接受的角度和方式,那这也是本文的意义所在。学习输入法相关知识再次强烈推荐Google官方示例SoftKeyboard Sample。

最后,生有涯而知无涯,如有纰漏之处还望广大读者批评指正,本APP源码已开源至GitHub(https://github.com/QiaoJianCheng/Gifin),同时也说明了一些坑,欢迎感兴趣的同学前来Star。






参考:

[1].Android Developers(https://developer.android.com/guide/topics/text/creating-input-method.html)

[2].SoftKeyboard Sample(https://android.googlesource.com/platform/development/+/refs/heads/master/samples/SoftKeyboard/)


--------  END  ---------

推荐阅读

Android解析WMS之Window删除过程

爱奇艺Android客户端启动优化与分析

Android人脸识别之识别人脸特征

如果你喜欢我的文章,就给公众号加个星标吧,方便阅读。


  

听说有人不敢点这里 👇

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存