安卓widget是什么意思(android widget什么意思)

在 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