安卓輔助功能(無障礙)AccessibilityService實戰介紹
簡要介紹
AccessibilityService是安卓平臺上提供的無障礙服務,用於幫助殘障人士使用手機,不過通過此功能可以完成很多事情.可以進行模擬介面操作,如點選介面上某個按鈕等.
使用詳細步驟
-
增加service定義
在onAccessibilityEvent函式中進行相應操作(安卓介面有變化時,都會呼叫此函式)
class MyAccessibilityService : AccessibilityService() { private val TAG = MyAccessibilityService::class.java.simpleName private var mContext: Context? = null override fun onCreate() { super.onCreate() Log.d(TAG, "onCreate") mContext = applicationContext AccessibilityOperator.getInstance().init(this) } override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { return Service.START_STICKY } override fun onAccessibilityEvent(event: AccessibilityEvent) { AccessibilityOperator.getInstance().updateEvent(event) val packageName = AccessibilityOperator.getInstance().rootNodeInfo?.packageName?.toString() //pasteToEditTextContent(packageName) var accessibilityService = AccessibilityOperator.getInstance() //按下返回鍵 //accessibilityService.performGlobalAction(GLOBAL_ACTION_BACK) //向下拉出狀態列 //accessibilityService.performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS) //向下拉出狀態列並顯示出所有的快捷操作按鈕 //accessibilityService.performGlobalAction(GLOBAL_ACTION_QUICK_SETTINGS) //按下HOME鍵 //accessibilityService.performGlobalAction(GLOBAL_ACTION_HOME) //顯示最近任務 //accessibilityService.performGlobalAction(GLOBAL_ACTION_RECENTS) //長按電源鍵 //accessibilityService.performGlobalAction(GLOBAL_ACTION_POWER_DIALOG) //分屏 //accessibilityService.performGlobalAction(GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN) //鎖屏(安卓9.0適用) //accessibilityService.performGlobalAction(GLOBAL_ACTION_LOCK_SCREEN) //截圖(安卓9.0適用) //accessibilityService.performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT) //開啟快速設定 accessibilityService.performGlobalAction(GLOBAL_ACTION_QUICK_SETTINGS) } /** * 修改EditText輸入框內容。 * 下面樣例修改了QQ搜尋輸入框內容。 */ private fun changeEditTextContent(packageName: String?) { getNodeToOperate(packageName)?.let { val arguments = Bundle() arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, "被無障礙服務修改啦") it.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments) } } /** * 讀取剪貼簿內容,貼上到EditText輸入框。 * 下面樣例修改了QQ搜尋輸入框內容。 */ private fun pasteToEditTextContent(packageName: String?) { getNodeToOperate(packageName)?.let { it.performAction(AccessibilityNodeInfo.FOCUS_INPUT) it.performAction(AccessibilityNodeInfo.ACTION_PASTE) it.recycle() } } private fun getNodeToOperate(packageName: String?): AccessibilityNodeInfo? { if (packageName != null && packageName == "com.tencent.mobileqq") { val nodes = AccessibilityOperator.getInstance().findNodesById("com.tencent.mobileqq:id/et_search_keyword") if (nodes != null && nodes.isNotEmpty()) { return nodes[0] } } return null } override fun onInterrupt() { } }
- 在Manifest檔案中增加service定義
<application> ...... <service android:name=".MyAccessibilityService" android:enabled="true" android:exported="true" android:label="@string/accessibility_service_name" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service> </application>
- AccessibilityOperator工具類
class AccessibilityOperator private constructor() { private var mAccessibilityEvent: AccessibilityEvent? = null private var accessibilityService: AccessibilityService? = null val rootNodeInfo: AccessibilityNodeInfo? get() { var nodeInfo: AccessibilityNodeInfo? = null accessibilityService?.let { nodeInfo = accessibilityService!!.rootInActiveWindow } if (nodeInfo == null && mAccessibilityEvent != null) { nodeInfo = mAccessibilityEvent!!.source } return nodeInfo } fun init(service: AccessibilityService) { accessibilityService = service } fun updateEvent(event: AccessibilityEvent) { mAccessibilityEvent = event } /** * 根據Text搜尋所有符合條件的節點, 模糊搜尋方式 */ fun findNodesByText(text: String): List<AccessibilityNodeInfo>? { val nodeInfo = rootNodeInfo return nodeInfo?.findAccessibilityNodeInfosByText(text) } /** * 根據View的ID搜尋符合條件的節點,精確搜尋方式; * 這個只適用於自己寫的介面,因為ID可能重複 * * @param viewId */ fun findNodesById(viewId: String): List<AccessibilityNodeInfo>? { val nodeInfo = rootNodeInfo return nodeInfo?.findAccessibilityNodeInfosByViewId(viewId) } fun clickByText(text: String): Boolean { return performClick(findNodesByText(text)) } fun clickParentByText(text: String, depth: Int): Boolean { return this.performClick(this.findParentNodesByText(text, depth)) } fun clickParentById(viewId: String, depth: Int): Boolean { return this.performClick(this.findParentNodesById(viewId, depth)) } fun findParentNodesByText(text: String, depth: Int): List<AccessibilityNodeInfo> { val rootNodeInfo = this.rootNodeInfo val resultNodeList = mutableListOf<AccessibilityNodeInfo>() if (rootNodeInfo != null) { val nodeList = findAccessibilityNodeInfosByText(rootNodeInfo, text) val iterator = nodeList.iterator() while (iterator.hasNext()) { val accessibilityNodeInfo = iterator.next() as AccessibilityNodeInfo resultNodeList.add(getParentNode(accessibilityNodeInfo, depth)) } } return resultNodeList } fun findParentNodesById(viewId: String, depth: Int): List<AccessibilityNodeInfo> { val rootNodeInfo = this.rootNodeInfo val resultNodeList = mutableListOf<AccessibilityNodeInfo>() if (rootNodeInfo != null) { val nodeList = rootNodeInfo.findAccessibilityNodeInfosByViewId(viewId) val iterator = nodeList.iterator() while (iterator.hasNext()) { val accessibilityNodeInfo = iterator.next() as AccessibilityNodeInfo resultNodeList.add(this.getParentNode(accessibilityNodeInfo, depth)) } } return resultNodeList } private fun findAccessibilityNodeInfosByText(node: AccessibilityNodeInfo?, text: String?): List<AccessibilityNodeInfo> { val resultNodeList = mutableListOf<AccessibilityNodeInfo>() if (node != null && text != null) { val nodeList = node.findAccessibilityNodeInfosByText(text) if (nodeList != null && !nodeList.isEmpty()) { val iterator = nodeList.iterator() while (iterator.hasNext()) { val nodeInList = iterator.next() as AccessibilityNodeInfo if (TextUtils.equals(nodeInList.text, text)) { resultNodeList.add(nodeInList) } } } return resultNodeList } else { return resultNodeList } } private fun getParentNode(nodeInfo: AccessibilityNodeInfo, depth: Int): AccessibilityNodeInfo { var resultNodeInfo = nodeInfo for (i in 0 until depth) { val parentNode = resultNodeInfo.parent resultNodeInfo = parentNode } return resultNodeInfo } /** * 根據View的ID搜尋符合條件的節點,精確搜尋方式; * 這個只適用於自己寫的介面,因為ID可能重複 * * @param viewId * @return 是否點選成功 */ fun clickById(viewId: String): Boolean { return performClick(findNodesById(viewId)) } private fun performClick(nodeInfoList: List<AccessibilityNodeInfo>?): Boolean { if (nodeInfoList != null && !nodeInfoList.isEmpty()) { var node: AccessibilityNodeInfo for (i in nodeInfoList.indices) { node = nodeInfoList[i] // 進行模擬點選 if (node.isEnabled) { return node.performAction(AccessibilityNodeInfo.ACTION_CLICK) } } } return false } fun clickBackKey(): Boolean { return performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) } fun performGlobalAction(action: Int): Boolean { return accessibilityService!!.performGlobalAction(action) } private fun getNodeInfo(nodeInfo: AccessibilityNodeInfo?): String { var result = "" if (nodeInfo != null) { result = nodeInfo.className.toString() + ";text:" + nodeInfo.text + ";id:" + nodeInfo.viewIdResourceName + ";" } return result } fun clickTextParent(text: String): Boolean { val nodeInfo = rootNodeInfo return nodeInfo?.let { clickTextParent(it, text) } ?: false } private fun clickTextParent(rootInfo: AccessibilityNodeInfo?, text: String): Boolean { if (rootInfo != null && !TextUtils.isEmpty(rootInfo.className)) { if ("android.widget.TextView" == rootInfo.className.toString()) { if (!TextUtils.isEmpty(rootInfo.text) && rootInfo.text.toString().startsWith(text)) { val result = performClick(rootInfo.parent) Log.v(TAG, rootInfo.parent.className.toString() + ":result=" + result) return result } } for (i in 0 until rootInfo.childCount) { val result = clickTextParent(rootInfo.getChild(i), text) if (result) { return result } } return false } return false } private fun performClick(targetInfo: AccessibilityNodeInfo): Boolean { return targetInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK) } companion object { private val TAG = "AccessibilityOperator" val instance = AccessibilityOperator() } }
Demo原始碼
https://gitee.com/cxyzy1/accessibilityDemo
安卓開發技術分享: https://www.jianshu.com/p/442339952f26
更多技術總結好文,請關注:「程式園中猿」
