转载注明出处:简书-十个雨点
在准备实现Bigbang的功能的时候,第一个需要解决的重大问题就是——如何像在锤子手机上一样方便的取词。好在有个同事做过辅助服务相关的功能,给我们提供了一个解决方案:通过辅助服务能够获取对View的点击和长按事件,并取得View的内容。
以此为起点,我们先实现了基于辅助服务的取词(适用于QQ、微信、支付宝等),然后加入了基于复制的取词(适用于浏览器、阅读器等),再又加入了全局复制功能(适用于系统设置等无法复制的页面),最后则是加上了截图OCR(适用于其他场景)。至此,基本上涵盖了所有取词的需要。
这些取词方式我都会一一介绍,这篇先介绍如何通过辅助模式取词,效果如下图所示:
也可以下载全能分词体验
1. 如何使用辅助服务
首先要在AndroidManifest.xml中声明:
<service
android:name=".component.service.BigBangMonitorService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:process=":monitor">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility" />
</service>
然后在res/xml/文件夹下新建文件accessibility.xml,内容如下:
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeViewClicked|typeViewLongClicked|typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagRetrieveInteractiveWindows"
android:canRetrieveWindowContent="true"
android:canRequestFilterKeyEvents ="true"
android:notificationTimeout="10"
android:packageNames="@null"
android:description="@string/accessibility_des"
android:settingsActivity="com.forfun.bigbang.SettingActivity"
/>
其中accessibilityEventTypes代表希望接收的事件类型,看名字就知道我们需要的是单击和长按,至于typeWindowStateChanged,则是在用于在切换activity时接收事件用的。
canRequestFilterKeyEvents是代表希望接收按键的事件类型,比如按音量键等,这里设置成true跟当前介绍的功能无关,而是为了用按键触发悬浮窗菜单,以后另开一篇介绍。
其他flag的含义可以参考API文档,这里就不展开说了。
最后创建BigBangMonitorService,本文用到的最重要的方法如下:
public class BigBangMonitorServiceextends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int type=event.getEventType();
switch (type){
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
case TYPE_VIEW_CLICKED:
case TYPE_VIEW_LONG_CLICKED:
break;
}
}
@Override
protected void onServiceConnected() {
super.onServiceConnected();
setServiceInfo(mAccessibilityServiceInfo);
}
}
其中onAccessibilityEvent很明显就是我们接收事件的回调方法。
而onServiceConnected则是在本service被设置成AccessibilityService 时的回调。什么意思呢?因为AccessibilityService 本身也是一个service,可以被start,bind,而只有当用户在辅助辅助的设置页面中开启了本程序的辅助服务时,才会被作为AccessibilityService使用,此时才会回调onServiceConnected,如下图。
2. 如何获取和处理点击事件
从前面的xml中,我们就已经设置好了需要获取的事件:单击和长按。所以在用户进行操作的时候我们就会收到相应的回调,注意这里的回调是异步回调,也就是说,我们没有办法对点击事件进行任何干预,只是收到一份通知而已。
那我们怎样从这个通知中取得我们想要的信息呢?直接看代码吧:
private CharSequence mWindowClassName;
private String mCurrentPackage;
private int mCurrentType;
private Map<String,Integer> selections;//保存每个应用的包名对应的触发方式
private boolean onlyText = true;
public int double_click_interval = 1000;
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
int type=event.getEventType();
switch (type){
case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
mWindowClassName = event.getClassName();
mCurrentPackage = event.getPackageName()==null?"":event.getPackageName().toString();
Integer selectType=selections.get(mCurrentPackage);
mCurrentType = selectType==null?TYPE_VIEW_NONE:(selectType+1);
break;
case TYPE_VIEW_CLICKED:
case TYPE_VIEW_LONG_CLICKED:
getText(event);
break;
}
private synchronized void getText(AccessibilityEvent event){
int type=getClickType(event);
CharSequence className = event.getClassName();
if (mWindowClassName==null){
return;
}
if (mWindowClassName.toString().startsWith("com.forfan.bigbang")){
//自己的应用不监控
return;
}
if (mCurrentPackage.equals(event.getPackageName())){
if (type!=mCurrentType){
//点击方式不匹配,直接返回
return;
}
}else {
//包名不匹配,直接返回
return;
}
if (className==null || className.equals("android.widget.EditText")){
//输入框不监控
return;
}
if (onlyText){
//onlyText方式下,只获取TextView的内容
if (className==null || !className.equals("android.widget.TextView")){
if (!hasShowTipToast){
ToastUtil.show(R.string.toast_tip_content);
hasShowTipToast=true;
}
return;
}
}
AccessibilityNodeInfo info=event.getSource();
if(info==null){
return;
}
CharSequence txt=info.getText();
if (TextUtils.isEmpty(txt) && !onlyText){
//非onlyText方式下获取文字更多,但是可能并不是想要的文字
//比如系统短信页面需要这样才能获取到内容。
List<CharSequence> txts=event.getText();
if (txts!=null) {
StringBuilder sb=new StringBuilder();
for (CharSequence t : txts) {
sb.append(t);
}
txt=sb.toString();
}
}
if (!TextUtils.isEmpty(txt)) {
if (txt.length()<=2 ){
//对于太短的词进行屏蔽,因为这些词往往是“发送”等功能按钮,其实应该根据不同的activity进行区分
if (!hasShowTooShortToast) {
ToastUtil.show(R.string.too_short_to_split);
hasShowTooShortToast = true;
}
return;
}
//打开分词功能
Intent intent=new Intent(this, BigBangActivity.class);
intent.addFlags(intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(BigBangActivity.TO_SPLIT_STR,txt.toString());
startActivity(intent);
}
}
private Method getSourceNodeIdMethod;
private long mLastSourceNodeId;
private long mLastClickTime;
private long getSourceNodeId(AccessibilityEvent event) {
//用于获取点击的View的id,用于检测双击操作
if (getSourceNodeIdMethod==null) {
Class<AccessibilityEvent> eventClass = AccessibilityEvent.class;
try {
getSourceNodeIdMethod = eventClass.getMethod("getSourceNodeId");
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
if (getSourceNodeIdMethod!=null) {
try {
return (long) getSourceNodeIdMethod.invoke(event);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return -1;
}
private int getClickType(AccessibilityEvent event){
int type = event.getEventType();
long time = event.getEventTime();
long id=getSourceNodeId(event);
if (type!=TYPE_VIEW_CLICKED){
mLastClickTime=time;
mLastSourceNodeId=-1;
return type;
}
if (id==-1){
mLastClickTime=time;
mLastSourceNodeId=-1;
return type;
}
if (type==TYPE_VIEW_CLICKED && time - mLastClickTime<= double_click_interval && id==mLastSourceNodeId){
mLastClickTime=-1;
mLastSourceNodeId=-1;
return TYPE_VIEW_DOUBLD_CLICKED;
}else {
mLastClickTime=time;
mLastSourceNodeId=id;
return type;
}
}
别看代码挺长,其实挺简单的,这么长的原因是实现了双击的检测(通过getClickType和getSourceNodeId实现的),只是对系统提供的API的一些灵活调用而已,没有什么难的地方。
3.不足之处
通过阅读上面的代码,不难看出辅助模式取词的两个局限:
- 点击的View必须是支持辅助服务的,也就是实现了sendAccessibilityEvent()、createAccessibilityNodeInfo()等方法的,而如果是我们自己绘制的View,都是无法使用的(除非非常有节操的程序员开发的)。不过好在大部分情况下,我们都是还是使用系统组件,或者是继承自系统组件。
- 只能获取到可点击的View的事件,对于不可点击的View则无能为力。特别是长按事件,必须设置了OnLongClickListener才能触发长按事件。这就导致了,在很多页面(比如系统设置页面)中,如果想监听单击或者双击,就会直接触发单击事件发生页面跳转,而长按则根本无法监听。
由于这两点原因,导致辅助模式取词最适合于QQ、微信、短信等以对话形式出现的文字。因此我们才又添加了各种其他的取词方式作为补充。
源码
完整代码可以参考Bigbang项目的BigBangMonitorService类。
ps:BigBangMonitorService中还包含了全局复制的功能和监听系统按键的功能,阅读的时候不要被干扰了,感兴趣的可以看——使用辅助服务实现全局复制和使用辅助服务监听系统按键这两篇文章
我们还基于Xposed框架实现了文字点击触发和全局复制:
如何通过Xposed框架获取点击的文字
使用Xposed框架实现全局复制