在 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
网友评论