kotlin Android AccessibilityService 無障礙入門

安卓的無障礙模式可以很好的進行自動化操作以幫助視障人士自動化完成一些任務。

無障礙可以做到,監聽屏幕變化,朗讀文本,定位以及操作控件等。

以下從配置到代碼依次進行無障礙設置與教程。

一、配置 AndroidManifest.xml

無障礙是個服務,因此需要再 AndroidManifest.xml 進行聲明等配置。包括申請權限,聲明服務等

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /><uses-permission android:name="android.permission.ACCESSIBILITY_SERVICE" /><application><serviceandroid:name="io.github.zimoyin.asdk.accessibility.AutoSdkAccessibilityService"android:exported="true"android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"><intent-filter><action android:name="android.accessibilityservice.AccessibilityService" /></intent-filter><meta-dataandroid:name="android.accessibilityservice"android:resource="@xml/accessibility_service_config" /></service></application>
  • android:name:無障礙服務類路徑
  • meta-data/android:resource:無障礙配置,需要創建 res/xml/accessibility_service_config.xml 文件

二、無障礙配置

需要創建 res/xml/accessibility_service_config.xml 文件

<accessibility-servicexmlns:android="http://schemas.android.com/apk/res/android"android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"android:accessibilityFeedbackType="feedbackSpoken"android:notificationTimeout="100"android:canPerformGestures="true"android:canRetrieveWindowContent="true"android:canRequestTouchExplorationMode="false"android:settingsActivity="true"android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews|flagReportViewIds"android:description="@string/accessibility_service_description" />
  • accessibilityEventTypes: 監聽事件類型,比如窗口滑動,彈窗,窗體變化,點擊等事件監聽,typeAllMask 則說監聽全部事件,盡可能合理的配置事件以減少電量消耗,減少服務頻繁喚醒
  • accessibilityFeedbackType: 回顯給用戶的方式(例如:配置TTS引擎,實現發音)
  • accessibilityFlags: 決定無障礙服務如何響應用戶操作、事件監聽范圍以及對界面元素的訪問權限
  • canPerformGestures:用于允許服務模擬用戶的復雜手勢操作 (如滑動、點擊、長按等)(API 24新增)
  • description: 無障礙描述,這里需要在 res/value/string.xml下配置
  • notificationTimeout:響應事件間隔,單位 ms
  • canRetrieveWindowContent: 是否能讀取窗口內容
  • settingsActivity: 允許用戶在系統設置中通過點擊你的無障礙服務名稱,跳轉到自定義的配置界面

三、無障礙描述配置

res/values/strings.xml

<resources><string name="accessibility_service_description">This is a accessibility service for AutoSDK.</string>
</resources>

四、代碼

1. 打開系統配置頁

        /*** 打開系統設置頁面,跳轉到輔助功能頁面* @param context 上下文,可傳入 Activity 或 Application*/fun openAccessibilitySettings(context: Context) {val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)intent.flags = Intent.FLAG_ACTIVITY_NEW_TASKcontext.startActivity(intent)}

2. 是否打開了無障礙配置

        /*** 檢查當前輔助功能服務是否已啟用*/fun isAccessibilityServiceEnabled(context: Context): Boolean {return getAccessibilityManager(context)?.isEnabled == true}/*** 獲取 AccessibilityManager*/fun getAccessibilityManager(context: Context): AccessibilityManager? {return context.getSystemService(ACCESSIBILITY_SERVICE) as? AccessibilityManager}

3. 繼承 AccessibilityService 并暴露實例對象

簽名代碼可以沒有但是,AccessibilityService 是一定要繼承的,并且類位置要與 AndroidManifest.xml 中聲明的位置一致


class AutoSdkAccessibilityService : AccessibilityService() {init {instance = this}companion object {val LAST_ID: String = AutoSdkAccessibilityService::class.java.name/*** 當前輔助功能服務實例*/var instance: AutoSdkAccessibilityService? = nullprivate set/*** 檢查 AndroidManifest.xml 是否存在 android.permission.SYSTEM_ALERT_WINDOW 權限*/fun hasSystemAlertWindowPermission(context: Context): Boolean {return isPermissionDeclared(context, Manifest.permission.SYSTEM_ALERT_WINDOW)}/*** 檢查 AndroidManifest.xml 是否存在 android.permission.ACCESSIBILITY_SERVICE 權限*/fun hasAccessibilityPermission(context: Context): Boolean {return isPermissionDeclared(context, "android.permission.ACCESSIBILITY_SERVICE")}/*** 檢查 AndroidManifest.xml 是否存在 android.permission.ACCESSIBILITY_SERVICE 權限*/@SuppressLint("QueryPermissionsNeeded")fun isAccessibilityServiceDeclared(context: Context): Boolean {val services = context.packageManager.queryIntentServices(Intent("android.accessibilityservice.AccessibilityService"),PackageManager.GET_META_DATA)for (serviceInfo in services) {if (serviceInfo.serviceInfo.packageName == context.packageName) {// 檢查是否聲明了 BIND_ACCESSIBILITY_SERVICE 權限if (serviceInfo.serviceInfo.permission != "android.permission.BIND_ACCESSIBILITY_SERVICE") {return false}// 檢查是否聲明了 meta-dataval metaData = serviceInfo.serviceInfo.metaDatareturn !(metaData == null || !metaData.containsKey("android.accessibilityservice"))}}return false}/*** 檢查是否聲明了指定權限*/private fun isPermissionDeclared(context: Context, permission: String): Boolean {return try {val packageInfo = context.packageManager.getPackageInfo(context.packageName,PackageManager.GET_PERMISSIONS)packageInfo.requestedPermissions?.contains(permission) == true} catch (e: Exception) {false}}/*** 檢查當前輔助功能服務是否已啟用*/fun isAccessibilityServiceEnabled(context: Context): Boolean {return getAccessibilityManager(context)?.isEnabled == true}/*** 獲取 AccessibilityManager*/fun getAccessibilityManager(context: Context): AccessibilityManager? {return context.getSystemService(ACCESSIBILITY_SERVICE) as? AccessibilityManager}/*** 獲取 AccessibilityServiceInfo*/fun getAccessibilityServiceInfo(context: Context): AccessibilityServiceInfo? {val accessibilityManager = getAccessibilityManager(context) ?: return nullval serviceInfo = accessibilityManager.installedAccessibilityServiceList.firstOrNull {it.id.endsWith(LAST_ID)}return serviceInfo}/*** 打開系統設置頁面,跳轉到輔助功能頁面* @param context 上下文,可傳入 Activity 或 Application*/fun openAccessibilitySettings(context: Context) {val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)intent.flags = Intent.FLAG_ACTIVITY_NEW_TASKcontext.startActivity(intent)}}/*** 當系統檢測到 UI 變化(如窗口更新、控件點擊)時才會觸發*/override fun onAccessibilityEvent(event: AccessibilityEvent) {AccessibilityListener.send(event)}override fun onInterrupt() {// TODO}/*** 模擬點擊* @param x x 坐標* @param y y 坐標*/fun clickAt(x: Float, y: Float) {val path = Path().apply {moveTo(x, y)lineTo(x, y)}val gestureDescription = GestureDescription.Builder().addStroke(GestureDescription.StrokeDescription(path, 0L, 100L)).build()dispatchGesture(gestureDescription, null, null)}/*** 模擬點擊* @param path 模擬點擊的路徑* @param start 模擬點擊的開始時間* @param end 模擬點擊的結束時間*/fun clickAt(path:Path, start: Long, end: Long) {val gestureDescription = GestureDescription.Builder().addStroke(GestureDescription.StrokeDescription(path, start, end)).build()dispatchGesture(gestureDescription, null, null)}
}

獲取根節點

AutoSdkAccessibilityService.instance?.rootInActiveWindow

遍歷節點

fun AccessibilityNodeInfo.forEach(callback: (AccessibilityNodeInfo) -> Unit) {for (i in 0 until childCount) {val node = getChild(i)callback(node)node.forEach(callback)}
}

節點查找 filter

fun AccessibilityNodeInfo.filter(callback: (AccessibilityNodeInfo) -> Boolean): MutableList<AccessibilityNodeInfo> {val list = mutableListOf<AccessibilityNodeInfo>()forEach {if (callback(it)) {list.add(it)}}return list
}

點擊節點


/*** 點擊節點范圍內的任意空間*/
fun AccessibilityNodeInfo?.click(service: AccessibilityService = requireNotNull(instance),minDuration: Long = 1L,maxDuration: Long = 200L
): Boolean {if (this == null) return falseval bounds = Rect().apply { this@click.getBoundsInScreen(this) }if (bounds.isEmpty) return false// 在控件邊界內生成隨機坐標val randomX = Random.nextInt(bounds.left, bounds.right)val randomY = Random.nextInt(bounds.top, bounds.bottom)val path = Path().apply {moveTo(randomX.toFloat(), randomY.toFloat())lineTo(randomX.toFloat(), randomY.toFloat())}val gesture = GestureDescription.Builder().addStroke(GestureDescription.StrokeDescription(path,0L,Random.nextLong(minDuration, maxDuration + 1))).build()return service.dispatchGesture(gesture,object : AccessibilityService.GestureResultCallback() {override fun onCancelled(gestureDescription: GestureDescription) {super.onCancelled(gestureDescription)}override fun onCompleted(gestureDescription: GestureDescription) {super.onCompleted(gestureDescription)}},null)
}/*** 點擊節點* 注意:點擊節點時,如果節點不可點擊,則會返回false。* 一般情況下在控件上面都會有圖標或者文本,如果匹配到了文本或者圖標,非特殊情況下是不能被點擊的,因此需要獲取父或者子節點進行點擊.* 推薦使用 [clickMatchNode]* @return 是否點擊成功*/
fun AccessibilityNodeInfo?.clickNode(): Boolean {return this?.performAction(AccessibilityNodeInfo.ACTION_CLICK) == true
}/*** 點擊節點* 注意:點擊節點時,如果節點不可點擊,則會查找父節點*/
fun AccessibilityNodeInfo?.clickMatchNode(): Boolean {if (this == null) return falsereturn if (isClickable) {performAction(AccessibilityNodeInfo.ACTION_CLICK)} else {parent.clickMatchNode()}
}/*** 長按節點* 注意:長按節點時,如果節點不可點擊,則會返回false。* 一般情況下在控件上面都會有圖標或者文本,如果匹配到了文本或者圖標,非特殊情況下是不能被點擊的,因此需要獲取父或者子節點進行點擊* 推薦使用 [longClickMatchNode]*/
fun AccessibilityNodeInfo?.longClickNode(): Boolean {return this?.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK) == true
}/*** 長按節點* 注意:長按節點時,如果節點不可點擊,則會查找父節點*/
fun AccessibilityNodeInfo?.longClickMatchNode(): Boolean {if (this == null) return falsereturn if (isClickable) {performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)} else {parent.longClickMatchNode()}
}

節點選擇器


/*** 包裝一個 AccessibilityNodeInfo 集合,提供鏈式條件過濾能力,仿照 Auto.js 的節點選擇器風格。** @property node 待過濾的節點列表*/
class AccessibilityNodeInfoWrapper(val node: AccessibilityNodeInfo) {private val conditions = mutableListOf<(AccessibilityNodeInfo) -> Boolean>()/*** 篩選文本等于 [text] 的節點。*/fun text(text: String): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString() == text }return this}/*** 篩選文本去除空格后等于 [text] 的節點。*/fun textTrim(): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString()?.trim() == it.text?.toString() }return this}/*** 篩選文本包含 [substr] 的節點。*/fun textContains(substr: String): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString()?.contains(substr) == true }return this}/*** 篩選文本匹配正則 [regex] 的節點。*/fun textMatches(regex: Regex): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString()?.matches(regex) == true }return this}/*** 篩選類名等于 [className] 的節點。*/fun className(className: String): AccessibilityNodeInfoWrapper {conditions += { it.className.toString() == className }return this}/*** 篩選資源 ID 等于 [id] 的節點。*/fun id(id: String): AccessibilityNodeInfoWrapper {conditions += { it.viewIdResourceName == id }return this}/*** 篩選包名等于 [packageName] 的節點。*/fun pkg(packageName: String): AccessibilityNodeInfoWrapper {conditions += { it.packageName == packageName }return this}/*** 篩選 contentDescription 等于 [desc] 的節點。*/fun description(desc: String): AccessibilityNodeInfoWrapper {conditions += { it.contentDescription?.toString() == desc }return this}/*** 篩選節點可點擊的。*/fun clickable(boolean: Boolean = true): AccessibilityNodeInfoWrapper {conditions += { it.isClickable == boolean}return this}/*** 篩選節點可見的(isVisibleToUser 為 true)。*/fun visible(): AccessibilityNodeInfoWrapper {conditions += { it.isVisibleToUser }return this}private fun conditionResult(info: AccessibilityNodeInfo): Boolean {for (condition in conditions) {if (!condition(info)) {return false}}return true}/*** 執行所有累積的條件過濾,返回符合條件的節點列表。* 調用完成后會清空已設置的條件,以便下一次重用。** @return 符合所有條件的節點列表*/fun done(): List<AccessibilityNodeInfo> = node.filter { info ->conditionResult(info)}.also {conditions.clear()}fun textStartsWith(string: String): AccessibilityNodeInfoWrapper {conditions += { it.text?.toString()?.startsWith(string) == true }return this}
}fun AccessibilityNodeInfo.selector(callback: AccessibilityNodeInfoWrapper.() -> Unit): List<AccessibilityNodeInfo> {return AccessibilityNodeInfoWrapper(this).apply { callback() }.done()
}fun AccessibilityNodeInfo.selector(): AccessibilityNodeInfoWrapper =AccessibilityNodeInfoWrapper(this)

事件分發

object AccessibilityListener {private val accessibilityListener = ConcurrentHashMap<UUID, (AccessibilityEvent) -> Unit>()fun onAccessibilityEvent(callback: (AccessibilityEvent) -> Unit) {val id = UUID.randomUUID()accessibilityListener[id] = callback}fun removeAccessibilityEvent(id: UUID) {accessibilityListener.remove(id)}fun send(event: AccessibilityEvent) {accessibilityListener.forEach {runCatching { it.value.invoke(event) }.onFailure {logger.error(it)}}}
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/83722.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/83722.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/83722.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Vue篇】數據秘語:從watch源碼看響應式宇宙的蝴蝶效應

目錄 引言 一、watch偵聽器&#xff08;監視器&#xff09; 1.作用&#xff1a; 2.語法&#xff1a; 3.偵聽器代碼準備 4. 配置項 5.總結 二、翻譯案例-代碼實現 1.需求 2.代碼實現 三、綜合案例——購物車案例 1. 需求 2. 代碼 引言 &#x1f4ac; 歡迎討論&#…

WPS中代碼段的識別方法及JS宏實現

在WPS中&#xff0c;文檔的基本結構可以通過對象模型來理解&#xff1a; &#xff08;1&#xff09;Document對象&#xff1a;表示整個文檔 &#xff08;2&#xff09;Range對象&#xff1a;表示文檔中的一段連續區域&#xff0c;可以是一個字符、一個句子或整個文檔 &#…

el-tree結合el-tree-transfer實現穿梭框里展示樹形數據

參考文章&#xff1a;我把他的彈框單拉出來一個獨立文件作為組件方便使用&#xff0c;遇到一些問題記錄一下。 testComponet.vue <template><div class"per_container"><div class"per_con_left"><div class"per_con_title&q…

Go 后端中雙 token 的實現模板

下面是一個典型的 Go 后端雙 Token 認證機制 實現模板&#xff0c;使用 Gin 框架 JWT Redis&#xff0c;結構清晰、可拓展&#xff0c;適合實戰開發。 項目結構建議 /utils├── jwt.go // Access & Refresh token 的生成和解析├── claims.go // 從請求…

Typescript學習教程,從入門到精通,TypeScript 對象語法知識點及案例代碼(7)

TypeScript 對象語法知識點及案例代碼 TypeScript 是 JavaScript 的超集&#xff0c;提供了靜態類型檢查和其他增強功能。在 TypeScript 中&#xff0c;對象是面向對象編程&#xff08;OOP&#xff09;的基礎。 一、對象概述 在 TypeScript 中&#xff0c;對象是屬性的集合&a…

應用BERT-GCN跨模態情緒分析:貿易緩和與金價波動的AI歸因

本文運用AI量化分析框架&#xff0c;結合市場情緒因子、宏觀經濟指標及技術面信號&#xff0c;對黃金與美元指數的聯動關系進行解析&#xff0c;揭示本輪貴金屬回調的深層驅動因素。 周三&#xff0c;現貨黃金價格單日跌幅達2.1%&#xff0c;盤中觸及3167.94美元/盎司關鍵價位&…

命令行登錄 MySQL 報 Segmentation fault 故障解決

問題描述&#xff1a;對 mysql8.0.35 源碼進行 make&#xff0c;由于一開始因為yum源問題少安裝依賴庫 庫&#xff0c;在鏈接時遇到錯誤 undefined reference to&#xff0c;后來安裝了相關依賴庫&#xff0c;再次 make 成功。于是將 mysqld 啟動&#xff0c;再用 mysql -u roo…

Axure設計數字鄉村可視化大屏:構建鄉村數據全景圖

今天&#xff0c;讓我們一同深入了解由Axure設計的數字鄉村可視化大屏&#xff0c;看看它如何通過精心的布局和多樣化的圖表類型&#xff0c;將鄉村的各類數據以直觀、易懂的方式呈現出來&#xff0c;為鄉村管理者提供有力的數據支持。 原型效果預覽鏈接&#xff1a;Axure數字鄉…

3D個人簡歷網站 4.小島

1.模型素材 在Sketchfab上下載狐貍島模型&#xff0c;然后轉換為素材資源asset&#xff0c;嫌麻煩直接在網盤鏈接下載素材&#xff0c; Fox’s islandshttps://sketchfab.com/3d-models/foxs-islands-163b68e09fcc47618450150be7785907https://gltf.pmnd.rs/ 素材夸克網盤&a…

智能開發工具PhpStorm v2025.1——增強AI輔助編碼功能

PhpStorm是一個輕量級且便捷的PHP IDE&#xff0c;其旨在提高用戶效率&#xff0c;可深刻理解用戶的編碼&#xff0c;提供智能代碼補全&#xff0c;快速導航以及即時錯誤檢查。可隨時幫助用戶對其編碼進行調整&#xff0c;運行單元測試或者提供可視化debug功能。 立即獲取PhpS…

Spark 的運行模式(--master) 和 部署方式(--deploy-mode)

Spark 的 運行模式&#xff08;--master&#xff09; 和 部署方式&#xff08;--deploy-mode&#xff09;&#xff0c;兩者的核心區別在于 資源調度范圍 和 Driver 進程的位置。 一、核心概念對比 維度--master&#xff08;運行模式&#xff09;--deploy-mode&#xff08;部署…

sqli—labs第八關——布爾盲注

一&#xff1a;確定注入類型 按照我們之前的步驟來 輸入 ?id1 and 11-- ?id1 and 12-- 界面正常 第二行界面異常空白 所以注入類型為單引號閉合型 二&#xff1a; 布爾盲注 1.判斷是否使用條件 &#xff08;1&#xff09;&#xff1a;存在注入但不會直接顯示查詢結果 …

ARP 原理總結

&#x1f310; 一、ARP 原理總結 ARP&#xff08;Address Resolution Protocol&#xff09;是用于通過 IP 地址解析 MAC 地址的協議&#xff0c;工作在 鏈路層 與 網絡層之間&#xff08;OSI 模型的第三層與第二層之間&#xff09;。 &#x1f501; ARP通信過程&#xff1a; …

SpringCloud——EureKa

目錄 1.前言 1.微服務拆分及遠程調用 3.EureKa注冊中心 遠程調用的問題 eureka原理 搭建EureKaServer 服務注冊 服務發現 1.前言 分布式架構&#xff1a;根據業務功能對系統進行拆分&#xff0c;每個業務模塊作為獨立項目開發&#xff0c;稱為服務。 優點&#xff1a; 降…

機頂盒刷機筆記

疑難雜癥解決 hitool線刷網口不通tftp超時--》關閉防火墻cm201-2卡刷所有包提示失敗abort install--》找個卡刷包只刷fastboot分區再卡刷就能通過了&#xff08;cm201救磚包 (M8273版子&#xff09;&#xff09; 刷機工具 海兔燒錄工具HiTool-STB-5.3.12工具&#xff0c;需要…

Linux動靜態庫制作與原理

什么是庫 庫是寫好的現有的&#xff0c;成熟的&#xff0c;可以復用的代碼。現實中每個程序都要依賴很多基礎的底層庫&#xff0c;不可能每個人的代碼都從零開始&#xff0c;因此庫的存在意義非同尋常。 本質上來說庫是一種可執行代碼的二進制形式&#xff0c;可以被操作系統…

如何通過小智AI制作會說話的機器人玩具?

一、硬件準備與組裝 1. 核心硬件選擇 主控芯片&#xff1a;選擇支持無線網絡連接、音頻處理和可編程接口的嵌入式開發板 音頻模塊&#xff1a;配備拾音麥克風與小型揚聲器&#xff0c;確保語音輸入/輸出功能 顯示模塊&#xff1a;選擇適配的交互顯示屏用于可視化反饋 擴展模…

如何控制郵件發送頻率避免打擾用戶

一、用戶行為 監測用戶與郵件的互動數據&#xff0c;如打開率、點擊率下滑或退訂申請增多&#xff0c;可能是發送頻率過高的警示信號。利用郵件營銷平臺的分析工具&#xff0c;識別這些指標的變動趨勢&#xff0c;為調整提供依據。 二、行業特性與受眾差異 不同行業用戶對郵…

定積分的“偶倍奇零”性質及其使用條件

定積分的“偶倍奇零”性質是針對對稱區間上的奇偶函數積分的重要簡化方法。以下是其核心內容和應用要點&#xff1a; ?一、基本性質 ?偶函數&#xff08;偶倍&#xff09;? 若 f(x) 在 [?a,a] 上為偶函數&#xff08;即 f(?x)f(x)&#xff09;&#xff0c;則&#xff1a; …

如何在 Windows 11 或 10 上安裝 Fliqlo 時鐘屏保

了解如何在 Windows 11 或 10 上安裝 Fliqlo,為您的 PC 或筆記本電腦屏幕添加一個翻轉時鐘屏保以顯示時間。 Fliqlo 是一款適用于 Windows 和 macOS 平臺的免費時鐘屏保。它也適用于移動設備,但僅限于 iPhone 和 iPad。Fliqlo 的主要功能是在用戶不活動時在 PC 或筆記本電腦…