通過(guò)輔助模式獲取點(diǎn)擊的文字

轉(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ò)輔助模式取詞,效果如下圖所示:

通過(guò)輔助服務(wù)實(shí)現(xiàn)雙擊觸發(fā)
通過(guò)輔助服務(wù)實(shí)現(xiàn)雙擊觸發(fā)

也可以下載全能分詞體驗(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,如下圖。

開(kāi)啟輔助服務(wù)的界面

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è)局限:

  1. 點(diǎn)擊的View必須是支持輔助服務(wù)的,也就是實(shí)現(xiàn)了sendAccessibilityEvent()、createAccessibilityNodeInfo()等方法的,而如果是我們自己繪制的View,都是無(wú)法使用的(除非非常有節(jié)操的程序員開(kāi)發(fā)的)。不過(guò)好在大部分情況下,我們都是還是使用系統(tǒng)組件,或者是繼承自系統(tǒng)組件。
  2. 只能獲取到可點(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ù)制

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容