一、序
Hi确垫,大家好弓颈,我是承香墨影!
Android 的輔助模式(Accessibility)功能非常的強大删掀∠杓剑基本上被獲取到授權(quán)之后,可以監(jiān)聽手機上的任何事件披泪,例如:屏幕點擊纤子、窗口的變化、以及模擬點擊付呕、模擬系統(tǒng)按鍵等等计福。
比較常見的實際使用例子,就是一般應用市場徽职,會推薦開啟輔助模式象颖,以便在安裝 Apk 的時候,自動幫你點擊“下一步”和“安裝”按鈕姆钉。還有個例子就是微信搶紅包插件说订,也是基于它來實現(xiàn)的。
Accessibility 的權(quán)限非常的高潮瓶,基本上你授權(quán)開啟某個別人提供的 AccessibilityService 之后陶冷,他就可以干很多事情而不讓你知道,而這些是不需要 Root 權(quán)限的毯辅。所以一般小體量的產(chǎn)品埂伦,可能支持它并沒有什么用,因為信任度太低了思恐,大部分用戶根本不會打開沾谜。比較常見的就是一些工具類的 App,幫用戶節(jié)省一些點擊的時間胀莹。
雖然很多時候基跑,Accessibility 不會被用在商業(yè)產(chǎn)品上,但是這并不妨礙我們使用 Accessibility 來做一些有意思的功能描焰。
二媳否、輔助模式的使用步驟
輔助模式是可以支持第三方開發(fā),也就是我們可以按照文檔對其進行支持,只要用戶授權(quán)開啟此服務篱竭,我們就可以利用 Accessibility 提供的一些標準 Api 實現(xiàn)很多有意思的功能力图。
如果你想要使用輔助模式,你還需要如下步驟:
- 實現(xiàn)一個繼承自 AccessibilityService 的服務類室抽。
- 設定配置信息搪哪,以便系統(tǒng)知道該輔助模式的一些基本信息,例如監(jiān)聽那些事件坪圾。
- 在清單文件(AndroidManifest.xml)中晓折,注冊此服務。
- 在系統(tǒng)設置中兽泄,找到“無障礙”漓概,并開啟此服務。
接下來我們一步一步講解這里的步驟和細節(jié)病梢。
2.1 繼承 AccessibilityService
輔助模式胃珍,本質(zhì)上還是一個服務返干,我們?nèi)绻胍С炙矗紫刃枰^承 AccessibilityService 這個類。
AccessibilityService 類提供了很多需要重寫的方法替裆,其中有兩個是強制重寫的:
public abstract void onAccessibilityEvent(AccessibilityEvent event);
public abstract void onInterrupt();
當開啟了某個 AccessibilityService 服務之后钮热,系統(tǒng)會在該服務監(jiān)聽的事件發(fā)生的時候填抬,回調(diào)它的 onAccessibilityEvent()
方法,并將該事件的信息當參數(shù)傳遞過去隧期,如果你監(jiān)聽的事件足夠多飒责,它就會被頻繁調(diào)用。
而 onInterrupt()
方法會在系統(tǒng)事件被打斷的時候回調(diào)仆潮,也是會被頻繁調(diào)用宏蛉,一般我們不需要做額外處理。
通常我們只需要在 onAccessibilityEvent()
方法中性置,編寫核心邏輯即可拾并,其他的方法,只是輔助使用鹏浅。
2.2 配置輔助模式
當創(chuàng)建一個 AccessibilityService 之后辟灰,我們還需要對其進行一些基本的配置,否則在系統(tǒng)設置的“無障礙”中篡石,是看不到我們編寫的服務的。
配置 AccessibilityService 有兩種方式西采,
- 通過 xml 配置文件
- 通過 Java 代碼中動態(tài)配置凰萨。
但是其實有一些屬性是只能通過 XML 配置文件進行配置的,Java 代碼只是讓某一些配置項更靈活了而已,后面會細說胖眷。
1武通、xml 配置文件
想要使用 XML 配置文件,首先需要創(chuàng)建一個 res/xml 的目錄珊搀,并在其內(nèi)創(chuàng)建一個 xml 文件冶忱,文件名隨意無要求,內(nèi)部定義一個 accessibility-service
標簽境析,在其中設定 AccessibilityService 的各項配置囚枪。例如我這里創(chuàng)建一個 accessibility_config.xml
的文件,后面會用到這個文件劳淆。
XML 配置 AccessibilityService 是我們一個比較常用的配置方法链沼,非常清晰且方便。
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackAllMask"
android:accessibilityFlags="flagReportViewIds"
android:canRetrieveWindowContent="true"
android:packageNames="com.forwarding.wechat"
android:description="@string/accessbility_desc"
android:notificationTimeout="100" />
例如上面就是一個常見的配置沛鸵,如果沒有特殊要求的話括勺,直接復制過去,修改一些個別參數(shù)就可以使用曲掰。
各項屬性的含義:
- accessibilityEventTypes:監(jiān)聽的事件類型疾捍,例如:typeAllMask 表示全部事件,而 typeViewClicked 表示只監(jiān)聽點擊事件栏妖。
- accessibilityFeedbackType:監(jiān)聽事件的反饋模式乱豆。
- canRetrieveWindowContent:是否允許獲取視圖層級的訪問權(quán),如果它被設置為 false底哥,
node.getSource()
方法會調(diào)用失敗咙鞍。 - accessibilityFlags:指定 Flag,一般用于指定根據(jù) Node 獲取 View ID 的權(quán)限趾徽。
- packageNames:開啟監(jiān)聽的應用包名续滋,可以指定多個包名,通過逗號“,”分割孵奶,不設置此屬性標識全局監(jiān)聽疲酌。
- description:輔助功能的描述,它會顯示在系統(tǒng)設置的“無障礙”中的描述信息中了袁。
- notificationTimeout:響應的毫秒數(shù)朗恳。
這些可配置的參數(shù),系統(tǒng)都提供了可選的配置參數(shù)载绿,正常不需要額外定制的時候粥诫,使用上面默認的配置即可,如果有定制需要崭庸,還是查閱官方文檔獲得最全的介紹怀浆。
AccessibilityService:
https://developer.android.com/reference/android/accessibilityservice/AccessibilityService
2谊囚、Java 代碼中動態(tài)配置
除了 XML 文件配置的方式,我們還可以通過重寫 AccessibilityService 的 onServiceConnected()
方法执赡,我們首先需要構(gòu)建一個 AccessibilityServiceInfo 對象镰踏,通過它的標準 Api 進行配置,再使用 setServiceInfo()
方法將它設置給輔助模式沙合。
onServiceConnected()
會在應用成功連接到此輔助服務的時候系統(tǒng)調(diào)用奠伪,一般在其中做一些初始化的操作即可。
override fun onServiceConnected() {
super.onServiceConnected()
var serviceInfo = AccessibilityServiceInfo()
serviceInfo.eventTypes = AccessibilityEvent.TYPES_ALL_MASK
serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
serviceInfo.notificationTimeout = 100
serviceInfo.packageNames = arrayOf("com.forwarding.wechat")
serviceInfo.flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
setServiceInfo(serviceInfo)
}
這里提供的例子首懈,其實和前面使用 XML 配置的效果一直绊率。推薦使用 XML 的配置方式,會更清晰且靈活猜拾,而且像 description 這種屬性即舌,在 AccessibilityServiceInfo 中,并沒有提供有效的類似 setDescription() 方法挎袜,這一點也確實是設計如此顽聂,畢竟服務沒有運行,就不存在描述信息盯仪,在系統(tǒng)設置的“無障礙”頁面紊搪,就讀取不到。
也就是說即便是使用 setServiceInfo()
方法動態(tài)設置全景,也逃不脫使用 XML 配置文件的方式耀石,我還是強烈建議都使用 XML 配置文件的方式配置輔助服務,主要是為了省事爸黄。
2.3 清單文件中注冊服務
本質(zhì)上 AccessibilityService 還是一個 Service滞伟,使用它我們還需要在清單文件中配置它。
<service android:label="承香墨影的輔助工具"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:name=".WeForwardServer">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService"/>
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_config"/>
</service>
這就是一個標準的 Service炕贵,其中 label
會被解析在系統(tǒng)設置的“輔助模式中顯示”梆奈,而 intent-filter
和 meta-data
按照格式寫就好了,沒什么原因称开。
meta-data
中亩钟,通過 android:resource
屬性指定的就是我們在第二步編輯的配置文件路徑,指定它就好了鳖轰。
2.4 開啟輔助模式
以上步驟都完成之后清酥,你就可以在系統(tǒng)的“無障礙”設置里,看到你編寫的輔助模式的開關了蕴侣。
默認為關閉狀態(tài)焰轻,打開它的時候,你會收到一個警告彈窗昆雀,說明當前你正在開啟一個無障礙的服務鹦马,它有哪些權(quán)限胧谈,這個對話框,我們是控制不了的荸频。
注意這里的 Title 就是清單文件里配置的 android:label
,而描述就是 XML 配置文件里的 android:description
信息客冈。
當你在系統(tǒng)設置里旭从,能看到此開關的時候,就說明你的輔助模式的服務场仲,配置的沒問題了和悦,接下來就要思考如何使用它。
三渠缕、編寫邏輯代碼
前面提到鸽素,在 AccessibilityService 里,我們最需要關注的就是 onAccessibilityEvent()
方法亦鳞,它會在我們監(jiān)聽的事件發(fā)生的時候馍忽,被系統(tǒng)回調(diào),并傳遞過來該事件相關的信息燕差。
接下來我們看看如何在 onAccessibilityEvent()
回調(diào)方法里遭笋,編寫具體的邏輯。
接下來 "程序員思維" 要上線了徒探,把大象關冰箱瓦呼,需要幾步。我們接下來來拆分輔助模式的步驟测暗。
- 判斷事件央串,
onAccessibilityEvent()
會被回調(diào)多次,而我們只需要處理我們關心的事件碗啄,其他的忽略過濾掉即可质和。 - 找到需要控制的關鍵節(jié)點(Node),以便之后進行控制挫掏。
- 對關鍵節(jié)點侦另,發(fā)送對于的操作事件,以便完成我們的步驟尉共。
- 回收資源褒傅,防止資源泄露。
很簡單對不對袄友,接下來我們細細的說下殿托,這些步驟相關的方法和屬性。
3.1 判斷事件
當 onAccessibilityEvent()
被系統(tǒng)回調(diào)的時候剧蚣,同時也會傳遞過來一個 AccessibilityEvent 對象支竹,它其中包含了很多與當前事件相關的信息旋廷,有興趣可以看看源碼,我們這里只關注最需要的幾個屬性礼搁。
1饶碘、eventType 判斷事件類型
通過 eventType 來判斷事件的類型,我們可以利用 getEventType()
方法獲取到它馒吴。
這些事件都很好辨認扎运,例如:TYPE_NOTIFICATION_STATE_CHANGED 是一個窗口 View 發(fā)生了變化,TYPE_VIEW_CLICKED 是某個 View 發(fā)生了一次點擊事件等等饮戳。
2豪治、packageName 判斷事件發(fā)生的 App
通過 getPackageName()
方法,判斷出事件發(fā)生在那個 App 里的扯罐。
3负拟、className 判斷當前發(fā)生事件的是那個類
通過 getClassName()
判斷當前發(fā)生事件的是那個類,例如 頁面的顯示歹河,className 可能指向一個 Activity掩浙,一個按鈕的點擊,className 可能指向的是一個 Button启泣,這些都是根據(jù)實際場景區(qū)分的涣脚。
4、text 判斷當前事件觸發(fā)源上的 Text
通過 getText()
獲取當前事件源的 text 屬性寥茫,可能是 TextView 的 Text遣蚀,也可能是 Activity 的 Label 屬性,依然是根據(jù)實際情況區(qū)分纱耻。
一般我們可以通過以上幾種方式芭梯,猜測是否是我們需要監(jiān)聽的事件,下一步就是我們找到我們要操作的源弄喘。
3.2 找到待控制的關鍵節(jié)點(Node)
通常我們是使用輔助模式去操作頁面上的某個元素玖喘,那這一步,就是為了找到它蘑志。
在輔助模式下累奈,頁面上的每個元素,其實都是一個個 AccessibilityNodeInfo 節(jié)點急但,它是一個類似樹形的結(jié)構(gòu)澎媒,其內(nèi)和我們真實 App 內(nèi)的布局層級是一致的,但是并不能將它單純的理解成一個 ViewTree波桩。
既然是樹形結(jié)構(gòu)戒努,我們首先要獲取到根節(jié)點的 NodeInfo,可以通過以下兩個方式獲雀涠恪:
- event.getSource()
- getRootInActiveWindow()
這兩個方法都會返回一個 AccessibilityNodeInfo 對象储玫。getSource()
是AccessibilityEvent 的方法侍筛,它可用的前提是前面配置 android:canRetrieveWindowContent
的時候,被設置為 True撒穷。所以我推薦使用 getRootInActiveWindow()
方法來獲取匣椰。這兩個方法還是略微有些差異,有興趣可以打斷點看看信息桥滨,但是大多數(shù)情況下窝爪,對我們使用者來說是一致的。
獲得根節(jié)點的 AccessibilityNodeInfo 之后齐媒,就可以通過它找到我們想操作的關鍵節(jié)點,在 AccessibilityNodeInfo 中纷跛,提供了以下兩個方法來找到關鍵節(jié)點喻括。
- findAccessibilityNodeInfosByViewId(String viewId)
- findAccessibilityNodeInfosByText(String text)
一個是依賴 ViewId,另外一個是依賴 Text 信息贫奠。
使用 ViewId 查找關鍵節(jié)點是穩(wěn)妥的方案唬血,而使用 Text 去查找,可能會找不到唤崭。
無論通過哪種方式查找 關鍵節(jié)點 拷恨,都是存在能找到多個 NodeInfo 的可能的,所以這兩個方法干脆的都返回了一個 List<AccessibilityNodeInfo>
谢肾,所以需要我們通過其他條件再過濾一遍腕侄,通常就是通過 Text 信息過濾。
var mNodeInfo = rootInActiveWindow
var listItem = mNodeInfo.findAccessibilityNodeInfosByViewId("com.tencent.mm:id/lp")
for (item in listItem) {
if (item.text.toString().equals("承香墨影")){
nodeClick(item)
}
}
如果是使用 findXxxByText()
的方法的話芦疏,還需要注意它實際上不是通過類似 ==
或者 equals()
的方法來查找子節(jié)點的冕杠,而是通過類似 contain()
的方式,所以只要節(jié)點的 text 屬性包含查找的內(nèi)容酸茴,都會被找到分预,這個我們額外還需要增加判斷條件。
如果這些方法都試過薪捍,還是找不到關鍵節(jié)點笼痹,可以通過遍歷的方式查找。
AccessibilityNodeInfo 既然是一個樹狀結(jié)構(gòu)酪穿,也提供了我們遍歷樹的方法凳干。
- getParent():查找父節(jié)點。
- getChild():返回子節(jié)點昆稿。
- getChildCount():當前節(jié)點的子節(jié)點個數(shù)纺座。
通過 getChild()
和 getChildCount()
兩個方法,我們是可以對整個 ViewNodeTree 進行遍歷溉潭,來找到我們關注的關鍵節(jié)點净响,這是一個最后的方案少欺,并不推薦使用。
3.3 觸發(fā)事件
輔助模式一般都是幫助我們響應一些事件馋贤,而這些事件大體上赞别,可以分為兩類。
- 全局系統(tǒng)事件配乓。
- View 事件仿滔。
對于全局系統(tǒng)事件,其實我們并不需要第二步找到的關鍵節(jié)點犹芹。AccessibilityService 提供了一個 performGlobalAction()
方法崎页,我們可以通過該方法,操作一些全局的系統(tǒng)事件腰埂,例如:模擬返回鍵點擊飒焦、模擬 HOME 鍵點擊、鎖屏等等屿笼。
// 返回鍵
performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK);
// HOME鍵
performGlobalAction(AccessibilityService.GLOBAL_ACTION_HOME);
這些事件被封裝在 AccessibilityService 中牺荠,以 GLOBAL_
為前綴,看看屬性說明就懂了驴一。
除了全局系統(tǒng)事件之外休雌,通常我們就是想操作第二步拿到的關鍵節(jié)點。
在 AccessibilityNodeInfo 中肝断,提供了一個 performAction()
的方法杈曲,可以通過該方法,對關鍵節(jié)點傳遞一個我們需要的事件孝情。
這些事件都被定義在 AccessibilityNodeInfo 中鱼蝉,以 ACTION_
為前綴定義。例如:ACTION_CLICK 是一個點擊事件箫荡,ACTION_SET_TEXT 設置一個輸入魁亦。
這里僅介紹一些比較常見的操作,更多的操作也是類似的使用方式羔挡。
1. View 的點擊
找到關鍵節(jié)點之后洁奈,就可以發(fā)送 AccessibilityNodeInfo.ACTION_CLICK
模擬對這個 View 的點擊操作。
但是有時候它是不生效的绞灼,主要原因是因為你找到的這個關鍵節(jié)點利术,它的 isClickable()
為 false。
例如微信的這個公眾號分享彈窗低矮,如果我們想要查找“發(fā)送給朋友”印叁,其實最好的辦法是找到這個 TextView 控件所代表的關鍵節(jié)點(NodeInfo),然后對它進行點擊。而實際上這個 TextView 是不具有點擊效果的轮蜕,它的 isClickable()
為 false昨悼。
這個時候可以想一個折中的方案,去找關鍵節(jié)點(NodeInfo)的父節(jié)點跃洛,再去判斷它是否可點擊率触,可點擊則點擊它,否則繼續(xù)向上找汇竭。
private fun nodeClick(node : AccessibilityNodeInfo?){
var clickNode = node;
while (clickNode!=null){
if(clickNode.isClickable){
clickNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
break;
}
clickNode = node?.parent
}
}
雖然 AccessibilityNodeInfo 其實也開放了 setClickable()
方法葱蝗,但是我不建議操作它,有些時候會拋出一個異常细燎,不太穩(wěn)定两曼。
2. EditText 輸入文字
對 EditText 輸入文字,最少需要兩個參數(shù)玻驻,關鍵節(jié)點和輸入的文字合愈。這就需要用到 performAction()
的另外一個重載方法,它允許額外在傳遞一個 Bundle 來指定參數(shù)击狮。
private fun nodeSetText(node : AccessibilityNodeInfo?,text:String){
var argument = Bundle()
argument.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,text)
node?.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT,argument)
}
所有支持定義的額外參數(shù),都被定義在 AccessibilityNodeInfo 中益老,并以 ACTION_ARGUMENT_
為前綴定義彪蓬。
3. ListView 的滾動
AccessibilityNodeInfo 其實只能操作當前屏幕下可見的 節(jié)點,所以碰上 ListView 或者 RecycleView 這種列表捺萌,就需要對它進行滾動档冬。
滾動的事件有兩種:
- ACTION_SCROLL_FORWARD
- ACTION_SCROLL_BACKWARD
private fun nodeScrollList(node : AccessibilityNodeInfo?){
node?.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
}
一個前進一個后退,足夠使用了桃纯。
3.4 回收資源
在使用完 AccessibilityNodeInfo 之后酷誓,別忘了還需要調(diào)用 recycle()
方法,釋放資源态坦。
nodeInfo.recycle();
四盐数、小結(jié)
輔助模式如何使用,到現(xiàn)在已經(jīng)講解的非常清楚了伞梯,后面基本上就是靠自己的想象力來做小功能了玫氢。
利用輔助模式,發(fā)揮想象力谜诫,你也可以做出很多有意思的功能漾峡。
公眾號后臺回復成長『成長』,將會得到我準備的學習資料喻旷,也能回復『加群』生逸,一起學習進步;你還能回復『提問』,向我發(fā)起提問槽袄。
推薦閱讀:
- 小程序 UI 布局指南(一)
- 程序員的密碼管理之道
- 手動刷新 MediaStore烙无,保存的圖片立即出現(xiàn)在相冊中
- 偽代碼、幽默和 Google 的藝術掰伸!
- 漫畫:App 防止 Fiddler 抓包小技巧皱炉!