虚拟点击

自己练手顺便写了一个简单的炉石传说脚本:一键投降+下一局
代码核心主要是两个Service,一个是悬浮窗,一个是虚拟点击。目前悬浮窗只适配了Android 8.0及之后的版本。
由于炉石传说游戏不是用java写的,监控不到其界面变化,就拿不到id,所以这里通过模拟坐标点击来实现的,不同手机分辨率不同,坐标会有差异。
代码放在github上了:传送门

效果图

android无障碍模拟手势 安卓无障碍模拟点击_android无障碍模拟手势

AccessibilityService

AccessibilityService-无障碍服务,设计初衷在于帮助残障用户使用android设备和应用,在后台运行,可以监听用户界面的一些状态转换,例如页面切换、焦点改变、通知等,并在触发AccessibilityEvents时由系统接收回调。后来被开发者另辟蹊径,用于一些插件开发,比如微信红包助手,还有一些需要监听第三方应用的插件。

class MyAccessibilityService : AccessibilityService() {
   override fun onServiceConnected() {
       super.onServiceConnected()
       serviceInfo = AccessibilityServiceInfo()
   }
   override fun onInterrupt() { }

   override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
}

android无障碍模拟手势 安卓无障碍模拟点击_android无障碍模拟手势_02

AccessibilityEvent

//接收到系统发送AccessibilityEvent时的回调
   override fun onAccessibilityEvent(event: AccessibilityEvent) {
       // "onAccessibilityEvent:event:$event".d(TAG)
       analyzeEvent(event, rootInActiveWindow)
       event.recycle()
   }

android无障碍模拟手势 安卓无障碍模拟点击_android_03

AccessibilityNodeInfo

private fun analyzeNode(nodeInfo: AccessibilityNodeInfo){
       val viewId = nodeInfo.viewIdResourceName
       val nodeInfoListById = nodeInfo.findAccessibilityNodeInfosByViewId("")
       val nodeInfoListByText = nodeInfo.findAccessibilityNodeInfosByText("")
       nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK)
   }

android无障碍模拟手势 安卓无障碍模拟点击_xml_04

使用

创建服务类

class MyAccessibilityService : AccessibilityService() {
   override fun onServiceConnected() {
       super.onServiceConnected()
       serviceInfo = AccessibilityServiceInfo()
   }
   override fun onInterrupt() { }

   override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
}

声明服务

<service
           android:name=".service.MyAccessibilityService"
           android:label="虚拟点击测试"
           android:exported="true"
           android:enabled="true"
           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>

申请权限

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

if(isAccessibilitySettingsOn(this@MainActivity, MyAccessibilityService::class.java)){
               "已开启无障碍权限!".showToast(this@MainActivity)
           }else{
               startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
           }


/**
    * 判断有没有无障碍权限
    * */
  private fun isAccessibilitySettingsOn(mContext: Context, clazz: Class<out AccessibilityService?>): Boolean {
       var accessibilityEnabled = 0
       val service = mContext.packageName + "/" + clazz.canonicalName
       try {
           accessibilityEnabled = Settings.Secure.getInt(
               mContext.applicationContext.contentResolver,
               Settings.Secure.ACCESSIBILITY_ENABLED
           )
       } catch (e: SettingNotFoundException) {
           e.printStackTrace()
       }
       val mStringColonSplitter = SimpleStringSplitter(':')
       if (accessibilityEnabled == 1) {
           val settingValue = Settings.Secure.getString(
               mContext.applicationContext.contentResolver,
               Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
           )
           if (settingValue != null) {
               mStringColonSplitter.setString(settingValue)
               while (mStringColonSplitter.hasNext()) {
                   val accessibilityService = mStringColonSplitter.next()
                   if (accessibilityService.equals(service, ignoreCase = true)) {
                       return true
                   }
               }
           }
       }
       return false
   }

服务参数配置

accessibility_service_config.xml

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:accessibilityEventTypes="typeAllMask"
   android:accessibilityFeedbackType="feedbackGeneric"
   android:accessibilityFlags="flagDefault|flagReportViewIds|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews|flagRequestFilterKeyEvents"
   android:canRetrieveWindowContent="true"
   android:canRequestFilterKeyEvents="true"

   android:canPerformGestures="true"
   android:canRequestTouchExplorationMode="true"
   android:canRequestEnhancedWebAccessibility="true"

   android:description="@string/accessibility_service_description"
   android:notificationTimeout="100"/>
   <!--accessibilityEventTypes:表示该服务对界面中的哪些变化感兴趣,即哪些事件通知,比如窗口打开,滑动,
   焦点变化,长按等.具体的值可以在AccessibilityEvent类中查到,如typeAllMask表示接受所有的事件通知.-->
   <!--accessibilityFeedbackType:表示反馈方式,比如是语音播放,还是震动-->
   <!--canRetrieveWindowContent:表示该服务能否访问活动窗口中的内容.也就是如果你希望在服务中获取窗体内容的化,则需要设置其值为true.-->
   <!--notificationTimeout:接受事件的时间间隔,通常将其设置为100即可.-->
   <!--packageNames:表示对该服务是用来监听哪个包的产生的事件-->

点击:

val clickPath = Path()
       clickPath.moveTo(x, y)
       val builder = GestureDescription.Builder()
       val gestureDescription = builder.addStroke(GestureDescription.StrokeDescription(clickPath, 100, 50)).build()
       dispatchGesture(gestureDescription, object : GestureResultCallback(){}, null)

滑动:

val slidePath = Path()
       slidePath.moveTo(x, y)
       slidePath.lineTo(x2, y2)
       val builder = GestureDescription.Builder()
       val gestureDescription = builder.addStroke(GestureDescription.StrokeDescription(slidePath, 100, 1000)).build()
       dispatchGesture(gestureDescription, object : GestureResultCallback(){}, null)