轉(zhuǎn)載注明出處:簡(jiǎn)書(shū)-十個(gè)雨點(diǎn)
在準(zhǔn)備實(shí)現(xiàn)Bigbang的功能的時(shí)候,第一個(gè)需要解決的重大問(wèn)題就是——如何像在錘子手機(jī)上一樣方便的取詞。好在有個(gè)同事做過(guò)輔助服務(wù)相關(guān)的功能,給我們提供了一個(gè)解決方案:通過(guò)輔助服務(wù)能夠獲取對(duì)View的點(diǎn)擊和長(zhǎng)按事件,并取得View的內(nèi)容。
以此為起點(diǎn),我們先實(shí)現(xiàn)了基于輔助服務(wù)的取詞(適用于QQ、微信、支付寶等),然后加入了基于復(fù)制的取詞(適用于瀏覽器、閱讀器等),再又加入了全局復(fù)制功能(適用于系統(tǒng)設(shè)置等無(wú)法復(fù)制的頁(yè)面),最后則是加上了截圖OCR(適用于其他場(chǎng)景)。至此,基本上涵蓋了所有取詞的需要。
這些取詞方式我都會(huì)一一介紹,這篇先介紹如何通過(guò)輔助模式取詞,效果如下圖所示:

也可以下載全能分詞體驗(yàn)
1. 如何使用輔助服務(wù)
首先要在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,內(nèi)容如下:
<?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代表希望接收的事件類(lèi)型,看名字就知道我們需要的是單擊和長(zhǎng)按,至于typeWindowStateChanged,則是在用于在切換activity時(shí)接收事件用的。
canRequestFilterKeyEvents是代表希望接收按鍵的事件類(lèi)型,比如按音量鍵等,這里設(shè)置成true跟當(dāng)前介紹的功能無(wú)關(guān),而是為了用按鍵觸發(fā)懸浮窗菜單,以后另開(kāi)一篇介紹。
其他flag的含義可以參考API文檔,這里就不展開(kāi)說(shuō)了。
最后創(chuàng)建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很明顯就是我們接收事件的回調(diào)方法。
而onServiceConnected則是在本service被設(shè)置成AccessibilityService 時(shí)的回調(diào)。什么意思呢?因?yàn)锳ccessibilityService 本身也是一個(gè)service,可以被start,bind,而只有當(dāng)用戶(hù)在輔助輔助的設(shè)置頁(yè)面中開(kāi)啟了本程序的輔助服務(wù)時(shí),才會(huì)被作為AccessibilityService使用,此時(shí)才會(huì)回調(diào)onServiceConnected,如下圖。

2. 如何獲取和處理點(diǎn)擊事件
從前面的xml中,我們就已經(jīng)設(shè)置好了需要獲取的事件:?jiǎn)螕艉烷L(zhǎng)按。所以在用戶(hù)進(jìn)行操作的時(shí)候我們就會(huì)收到相應(yīng)的回調(diào),注意這里的回調(diào)是異步回調(diào),也就是說(shuō),我們沒(méi)有辦法對(duì)點(diǎn)擊事件進(jìn)行任何干預(yù),只是收到一份通知而已。
那我們?cè)鯓訌倪@個(gè)通知中取得我們想要的信息呢?直接看代碼吧:
private CharSequence mWindowClassName;
private String mCurrentPackage;
private int mCurrentType;
private Map<String,Integer> selections;//保存每個(gè)應(yīng)用的包名對(duì)應(yīng)的觸發(fā)方式
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")){
//自己的應(yīng)用不監(jiān)控
return;
}
if (mCurrentPackage.equals(event.getPackageName())){
if (type!=mCurrentType){
//點(diǎn)擊方式不匹配,直接返回
return;
}
}else {
//包名不匹配,直接返回
return;
}
if (className==null || className.equals("android.widget.EditText")){
//輸入框不監(jiān)控
return;
}
if (onlyText){
//onlyText方式下,只獲取TextView的內(nèi)容
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方式下獲取文字更多,但是可能并不是想要的文字
//比如系統(tǒng)短信頁(yè)面需要這樣才能獲取到內(nèi)容。
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 ){
//對(duì)于太短的詞進(jìn)行屏蔽,因?yàn)檫@些詞往往是“發(fā)送”等功能按鈕,其實(shí)應(yīng)該根據(jù)不同的activity進(jìn)行區(qū)分
if (!hasShowTooShortToast) {
ToastUtil.show(R.string.too_short_to_split);
hasShowTooShortToast = true;
}
return;
}
//打開(kāi)分詞功能
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) {
//用于獲取點(diǎn)擊的View的id,用于檢測(cè)雙擊操作
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;
}
}
別看代碼挺長(zhǎng),其實(shí)挺簡(jiǎn)單的,這么長(zhǎng)的原因是實(shí)現(xiàn)了雙擊的檢測(cè)(通過(guò)getClickType和getSourceNodeId實(shí)現(xiàn)的),只是對(duì)系統(tǒng)提供的API的一些靈活調(diào)用而已,沒(méi)有什么難的地方。
3.不足之處
通過(guò)閱讀上面的代碼,不難看出輔助模式取詞的兩個(gè)局限:
- 點(diǎn)擊的View必須是支持輔助服務(wù)的,也就是實(shí)現(xiàn)了sendAccessibilityEvent()、createAccessibilityNodeInfo()等方法的,而如果是我們自己繪制的View,都是無(wú)法使用的(除非非常有節(jié)操的程序員開(kāi)發(fā)的)。不過(guò)好在大部分情況下,我們都是還是使用系統(tǒng)組件,或者是繼承自系統(tǒng)組件。
- 只能獲取到可點(diǎn)擊的View的事件,對(duì)于不可點(diǎn)擊的View則無(wú)能為力。特別是長(zhǎng)按事件,必須設(shè)置了OnLongClickListener才能觸發(fā)長(zhǎng)按事件。這就導(dǎo)致了,在很多頁(yè)面(比如系統(tǒng)設(shè)置頁(yè)面)中,如果想監(jiān)聽(tīng)單擊或者雙擊,就會(huì)直接觸發(fā)單擊事件發(fā)生頁(yè)面跳轉(zhuǎn),而長(zhǎng)按則根本無(wú)法監(jiān)聽(tīng)。
由于這兩點(diǎn)原因,導(dǎo)致輔助模式取詞最適合于QQ、微信、短信等以對(duì)話(huà)形式出現(xiàn)的文字。因此我們才又添加了各種其他的取詞方式作為補(bǔ)充。
源碼
完整代碼可以參考Bigbang項(xiàng)目的BigBangMonitorService類(lèi)。
ps:BigBangMonitorService中還包含了全局復(fù)制的功能和監(jiān)聽(tīng)系統(tǒng)按鍵的功能,閱讀的時(shí)候不要被干擾了,感興趣的可以看——使用輔助服務(wù)實(shí)現(xiàn)全局復(fù)制和使用輔助服務(wù)監(jiān)聽(tīng)系統(tǒng)按鍵這兩篇文章
我們還基于Xposed框架實(shí)現(xiàn)了文字點(diǎn)擊觸發(fā)和全局復(fù)制:
如何通過(guò)Xposed框架獲取點(diǎn)擊的文字
使用Xposed框架實(shí)現(xiàn)全局復(fù)制