美文网首页Android Other
Android:辅助功能(模拟点击,模拟手势)入门基本学习

Android:辅助功能(模拟点击,模拟手势)入门基本学习

作者: android不是安卓 | 来源:发表于2022-06-21 19:33 被阅读0次

    在 Android 中,有个非常强大的功能,那就是辅助功能。辅助功能是用于服务残障人士的。比如对于视障人士来说,辅助功能可以帮助他们读出屏幕上的文字或图片(阅读图片时会播放其 ContentDescription 属性)。

    除此之外,辅助功能还可以模拟点击,模拟手势等等。

    这篇文章我们就来学习辅助功能的基本用法。

    一、新建 MyAccessibilityService 类

    首先,新建一个 MyAccessibilityService 类,继承自系统的 AccessibilityService 类:

    class MyAccessibilityService : AccessibilityService() {
        override fun onAccessibilityEvent(accessibilityEvent: AccessibilityEvent?) {
        }
    
        override fun onInterrupt() {
        }
    }
    

    继承 AccessibilityService 后,需要实现两个方法 onAccessibilityEvent 和 onInterrupt。

    onAccessibilityEvent 方法中,带有一个参数 AccessibilityEvent,当界面发生改变时,这个方法就会被调用,界面改变的具体信息就会包含在这个参数中。

    onInterrupt 方法辅助服务被中断了。

    我们暂时先在这两个方法中简单地打印一行日志,待会再在其中添加具体的功能。

    二、注册 Service

    写好 MyAccessibilityService 类后,需要在 AndroidManifest 中注册。注册辅助服务和注册一般的服务略有区别:

    <service
        android:name=".MyAccessibilityService"
        android:description="@string/description_in_manifest"
        android:exported="true"
        android:label="@string/label_in_manifest"
        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_config" />
    
    • 首先是需要声明一个 label,这个 label 是在系统的辅助功能设置中显示的名字
    • description 属性可以不写,指的是在辅助功能设置中显示的该辅助功能的描述
    • permission 属性必须写,表示这个服务需要绑定 AccssibilityService
    • 在这个 service 中,有一个 inter-filter,这个也是必须写的,不妨记作固定格式
    • 还有一个 meta-data,其中的 resource 属性指向一个 xml 文件,这个文件中可以配置允许这个辅助功能做哪些事

    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:canPerformGestures="true"
        android:canRetrieveWindowContent="true"
        android:description="@string/description_in_xml"
        android:notificationTimeout="100" />
    
    

    AndroidManifest 和 xml 中,用到的字符串资源文件如下:

    <string name="label_in_manifest">Label in manifest</string>
    <string name="description_in_manifest">Description in manifest</string>
    <string name="description_in_xml">Description in xml</string>
    
    

    这些都设置好之后,这个 Service 就注册成功了。

    现在就可以运行一下看看效果了。

    三、开启辅助服务

    此时运行程序,会发现没有任何 onAccessibilityEvent 事件打出。这是因为辅助功能是一项比较危险的功能,默认是关闭的。需要到系统设置中手动打开才可以使用。

    通过图中的三个步骤,确保 Use Label in manifest 的开关是打开的,我们的辅助功能就被正式启用了。

    从图中我们也可以看出注册 service 时写的字符串各自的使用场景。

    在程序中,也可以通过代码到达辅助功能设置页面,代码如下:

    object AccessibilitySettingUtils {
        fun jumpToAccessibilitySetting(context: Context) {
            val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
            context.startActivity(intent)
        }
    }
    

    开启辅助功能后,点击桌面就会在 Log 控制台收到以下消息:

    D/~~~: accessibilityEvent: EventType: TYPE_WINDOW_CONTENT_CHANGED; EventTime: 101990739; PackageName: com.google.android.apps.nexuslauncher; MovementGranularity: 0; Action: 0; ContentChangeTypes: [CONTENT_CHANGE_TYPE_SUBTREE]; WindowChangeTypes: [] [ ClassName: android.widget.FrameLayout; Text: []; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: false; Scrollable: false; BeforeText: null; FromIndex: -1; ToIndex: -1; ScrollX: 0; ScrollY: 0; MaxScrollX: 0; MaxScrollY: 0; ScrollDeltaX: -1; ScrollDeltaY: -1; AddedCount: -1; RemovedCount: -1; ParcelableData: null ]; recordCount: 0
    

    这表示我们接收到了一个 accessibilityEvent 消息,他的类型是 TYPE_WINDOW_CONTENT_CHANGED,意思是窗口内容发生了变化,PackageName 中表示这个变化的内容所在的包名。

    说明我们的辅助功能已经开始工作了。

    四、点击对应坐标

    想要查看屏幕上的坐标,可以在开发人员选项中打开显示坐标的设置:

    打开这个设置后,每次点击屏幕,都会在顶部显示当前点击的位置坐标。

    点击对应坐标的代码如下:

    object ClickUtils {
        fun click(accessibilityService: AccessibilityService, x: Float, y: Float) {
            Log.d("~~~", "click: ($x, $y)")
            val builder = GestureDescription.Builder()
            val path = Path()
            path.moveTo(x, y)
            path.lineTo(x, y)
            builder.addStroke(GestureDescription.StrokeDescription(path, 0, 1))
            val gesture = builder.build()
            accessibilityService.dispatchGesture(gesture, object : AccessibilityService.GestureResultCallback() {
                override fun onCancelled(gestureDescription: GestureDescription) {
                    super.onCancelled(gestureDescription)
                }
    
                override fun onCompleted(gestureDescription: GestureDescription) {
                    super.onCompleted(gestureDescription)
                }
            }, null)
        }
    }
    

    在这个工具类中,我们将 AccessibilityService 和坐标传入。

    通过 GestureDescription 的 Builder 构建一个手势,通过 Builder 的 addStoke 方法传入一条 path,这条 path 我们设置为从 (x, y) 坐标移动到 (x, y) 坐标。

    StrokeDescription 的后两个参数表示 startTime 和 duration,分别表示手势的开始时间以及持续时间,以毫秒为单位。我将其设置为 0 和 1,也就是 1ms 以内完成从 (x, y) 坐标移动到 (x, y) 坐标。

    这样就模拟出了一个点击事件。

    五、测试

    我们不妨写个简单的页面来测试一下。

    先写一个页面,包含两个按钮:

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/btn_jump_to_settings"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Jump to Settings"
            android:textAllCaps="false"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/btn_test"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Test"
            app:layout_constraintTop_toBottomOf="@id/btn_jump_to_settings" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    这个页面的效果图:

    在 app/build.gradle 中,开启 ViewBinding,目的是使用这些按钮更方便:

    buildFeatures {
        viewBinding true
    }
    

    在 MainActivity 中,设置按钮的点击事件:

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
            binding.btnJumpToSettings.setOnClickListener {
                AccessibilitySettingUtils.jumpToAccessibilitySetting(this)
            }
            binding.btnTest.setOnClickListener {
                Toast.makeText(this, "I'm clicked", Toast.LENGTH_SHORT).show()
            }
        }
    }
    
    • 第一个按钮 btnJumpToSettings 的作用是点击跳转到辅助服务设置页
    • 第二个按钮用来做测试,点击时会弹出 Toast:"I'm clicked"。待会我们就模拟点击这个按钮。

    查看一下第二个按钮的坐标位置:

    从图中可以看出,第三个按钮的坐标大约是 (622,406)。

    在 MyAccessibilityService 的 onServiceConnected 方法中,模拟点击此坐标:

    override fun onServiceConnected() {
        super.onServiceConnected()
        Log.d("~~~", "onServiceConnected")
        thread {
            Thread.sleep(5000)
            ClickUtils.click(this, 622f, 406f)
        }
    }
    

    可以看到,我们在 onServiceConnected 方法中,开启了一个线程,先睡眠 5s,再调用 ClickUtils.click(this, 622f, 406f) 方法点击 (622,406)。

    之所以要睡眠 5s 中,是因为在设置中开启了辅助服务后,onServiceConnected 方法就会立刻回调,而我们要从设置页面返回到此页面才能看到这个按钮被点击的效果,这个过程需要一点时间。

    开测:

    可以看到,我先点击了第一个按钮到达辅助服务设置页面,在开启辅助服务后,我立即返回了 MainActivity,等待几秒后,Test 按钮被自动点击了。说明我们的辅助点击功能已经正常工作了。

    六、后记

    辅助功能除了模拟点击,还有很多其他的玩法。由于点击时传入的是一条路径,所以它实际上可以用来模拟任意手势,并不局限于点击事件,本文只是用点击事件作为示例,希望读者可以举一反三。

    辅助功能还可以解析页面上的元素,通过其 id 或者 text 来找到 AccessibilityNodeInfo,再通过 AccessibilityNodeInfo 的 performAction 来模拟点击。但这种方式有较大的局限性,首先需要知道 view 的 id 或者 text,而且对于 WebView、GameView 等常常无能为力。所以我更喜欢用传入坐标的方式来模拟点击。

    需要注意的是,dispatchGesture 方法只能在 Android 7.0 以上使用,所以本文讲解的内容只适合 Android 7.0 以上。

    传入坐标的方式非常强大,它不局限于本应用内,它就像模拟出了一只手,可以在任何时刻帮助我们点击屏幕。缺点是它不知道当前页面显示的内容是什么,这一点可以通过图片识别来解决。

    我使用辅助服务主要是为了给自己的手机添加一些好玩的功能,配合上图片识别,可以做很多的事情,其乐无穷。

    作者:wkxjc
    链接:https://juejin.cn/post/7111372688392159268

    相关文章

      网友评论

        本文标题:Android:辅助功能(模拟点击,模拟手势)入门基本学习

        本文链接:https://www.haomeiwen.com/subject/fbmevrtx.html