做网站的公司都是小公司,网站开发用jquery吗,网站建设 cms,国内的c2c网站有哪些一、背景 本文仅用于做学习总结#xff0c;转换成自己的理解#xff0c;方便需要时快速查阅#xff0c;深入研究可以去官网了解更多#xff1a;官网链接点这里 之前对接AI语音功能时#xff0c;发现有些按钮#xff08;或文本#xff09;在我没有主动注册唤醒词场景…一、背景 本文仅用于做学习总结转换成自己的理解方便需要时快速查阅深入研究可以去官网了解更多官网链接点这里 之前对接AI语音功能时发现有些按钮或文本在我没有主动注册唤醒词场景下还是响应了点击使用profiler跟踪调用堆栈才发现是使用了无障碍服务实现的。因为开发的是系统应用也没必要主动去打开无障碍服务开关于是觉得无障碍服务有很大的可发挥空间于是借助无障碍服务实现了一个显示当前展示的Window/Activity/Dialog的悬浮窗用于演示无障碍服务的用法及其强大之处。
二、用法
2.1 新建一个继承AccessibilityService的无障碍服务类并按需重写回调方法
class AccessibilityTest : AccessibilityService() {override fun onAccessibilityEvent(event: AccessibilityEvent?) {Log.d(TAG, onAccessibilityEvent: event $event)}override fun onServiceConnected() {super.onServiceConnected()Log.d(TAG, onServiceConnected: )}override fun onUnbind(intent: Intent?): Boolean {Log.d(TAG, onUnbind: intent $intent)return super.onUnbind(intent)}override fun onInterrupt() {Log.d(TAG, onInterrupt: )}
}onServiceConnected(): 当无障碍服务打开后有注册的交互事件发生时如果还没有连接服务这会先执行连接并回调这个方法。 问题AccessibilityServie继承Service也就是普通的服务没有绑定前台的notification等可见的界面会不会在后台过一会儿就断开了是否会导致某些时候无法捕获到交互的事件onAccessibilityEvent(event: AccessibilityEvent?): 当有注册的交互事件比如点击、长按、焦点变化等触发时会回调这个函数 event.getEventType(): 获取事件类型点击TYPE_VIEW_CLICKED长按TYPE_VIEW_LONG_CLICKED窗口状态变化TYPE_WINDOW_STATE_CHANGED详细的可以查阅AccessibilityService类的EventType注解中有枚举出所有的事件类型。event.getPackName(): 获取交互来自的包名event.getClassName(): 如果是Activity/Dialog则是其类的全路径名如果是View的话则展示当前的View全路径名。 onUnbind(intent: Intent?): 断开服务时回调用于处理一些资源释放的逻辑。onInterrupt():
2.2 AndroidManifest.xml文件中注册无障碍服务 这个步骤和普通的Service注册有些不同需要配置permission、intent-filter和无障碍服务的xml配置文件基本都是固定的格式只是按需改一些配置项。
serviceandroid:name.accessibility.AccessibilityTestandroid:exportedtrueandroid:labelstring/accessibility_tipandroid:permissionandroid.permission.BIND_ACCESSIBILITY_SERVICEandroid:process:BackgroundServiceintent-filteraction android:nameandroid.accessibilityservice.AccessibilityService //intent-filtermeta-dataandroid:nameandroid.accessibilityserviceandroid:resourcexml/accessibility_config /
/serviceservice节点配置的label标签会在无障碍服务中展示比如上面的label内容是“accessibility_tip”那么在无障碍服务中展示就会如下所示“ 在/res/xml目录下新建一个accessibility_config.xml文件内容如下
?xml version1.0 encodingutf-8?
accessibility-service xmlns:androidhttp://schemas.android.com/apk/res/androidandroid:accessibilityEventTypestypeAllMaskandroid:accessibilityFeedbackTypefeedbackGenericandroid:canRetrieveWindowContenttrueandroid:descriptionstring/accessibilty_desc /⚠️如果这里指定了包名android:packageNames的值多个包名用英文逗号分隔。则只会收到对应包名应用的事件。
android:description属性设置的就是上面的无障碍中accessibility_tip服务最下面的文案介绍。android:accessibilityEventTypes指定接收的事件类型android:accessibilityFeedbackType指定接收的反馈类型
2.3 在AccessibilityTest的onServiceConnected方法中动态设置serviceInfo
AccessibilityTest-onServiceConnected(): 通过在Service连接到无障碍服务的回调调用setServiceInfo方法可以在在运行时调整无障碍服务的配置
override fun onServiceConnected() {super.onServiceConnected()Log.d(TAG, onServiceConnected: )val accessibilityServiceInfo AccessibilityServiceInfo()accessibilityServiceInfo.eventTypes (AccessibilityEvent.TYPE_WINDOWS_CHANGEDor AccessibilityEvent.TYPE_WINDOW_STATE_CHANGEDor AccessibilityEvent.TYPE_VIEW_CLICKEDor AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGEDor AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)accessibilityServiceInfo.feedbackType AccessibilityServiceInfo.FEEDBACK_ALL_MASKaccessibilityServiceInfo.notificationTimeout 0accessibilityServiceInfo.flags AccessibilityServiceInfo.DEFAULT// 如果这里指定了包名则只会收到对应包名应用的事件accessibilityServiceInfo.packageNames arrayOf(com.yanggui.animatortest)serviceInfo accessibilityServiceInfo
}完成以上的步骤然后编译运行安装到手机上然后从无障碍服务中开启就能够在AccessibilityTest的onAccessibilityEvent中收到各种交互事件了。
无障碍服务跑起来后会打印如下log
// 从无障碍服务中打开accessibility_tip服务回调onServiceConnected方法
14:25:34.170 D onServiceConnected:
// 打开当前应用会执行onAccessibilityEvent方法回调一系列的事件信息封装在// AccessibilityEvent中
14:26:44.792 D onAccessibilityEvent: event EventType: TYPE_VIEW_CLICKED; EventTime: 7897173; PackageName: com.google.android.apps.nexuslauncher; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: android.widget.TextView; Text: [AnimatorTest]; ContentDescription: AnimatorTest; 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
14:26:44.847 D onAccessibilityEvent: event EventType: TYPE_WINDOW_CONTENT_CHANGED; EventTime: 7897231; 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
14:26:44.891 D onAccessibilityEvent: event EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 7897277; PackageName: com.yanggui.animatortest; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: com.yanggui.animatortest.leanback.RowsSupportFragmentActivity; Text: [AnimatorTest]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: true; 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
14:26:44.952 D onAccessibilityEvent: event EventType: TYPE_WINDOW_STATE_CHANGED; EventTime: 7897337; PackageName: com.yanggui.animatortest; MovementGranularity: 0; Action: 0; ContentChangeTypes: []; WindowChangeTypes: [] [ ClassName: com.yanggui.animatortest.leanback.RowsSupportFragmentActivity; Text: [AnimatorTest]; ContentDescription: null; ItemCount: -1; CurrentItemIndex: -1; Enabled: true; Password: false; Checked: false; FullScreen: true; 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
// 从无障碍服务中关闭accessibility_tip服务回调onUnbind方法
14:24:38.264 D onUnbind: intent Intent { cmpcom.yanggui.animatortest/.accessibility.AccessibilityTest }2.4 附反馈类型feedbackType和事件类型eventType的枚举值
反馈类型feedbackType定义在AccessibilityServiceInfo中
IntDef(flag true, prefix { FEEDBACK_ }, value {FEEDBACK_AUDIBLE,FEEDBACK_GENERIC,FEEDBACK_HAPTIC,FEEDBACK_SPOKEN,FEEDBACK_VISUAL,FEEDBACK_BRAILLE
})
Retention(RetentionPolicy.SOURCE)
public interface FeedbackType {}事件类型eventType定义在AccessibilityEvent中
IntDef(flag true,prefix {TYPE_},value {TYPE_VIEW_CLICKED, // 点击事件TYPE_VIEW_LONG_CLICKED, // 长按事件TYPE_VIEW_SELECTED, // view选中TYPE_VIEW_FOCUSED, // view上焦使用遥控操作的需要关注该事件TYPE_VIEW_TEXT_CHANGED, // 表示更改 android.widget.EditText的文本的事件。TYPE_WINDOW_STATE_CHANGED,TYPE_NOTIFICATION_STATE_CHANGED,TYPE_VIEW_HOVER_ENTER,TYPE_VIEW_HOVER_EXIT,TYPE_TOUCH_EXPLORATION_GESTURE_START,TYPE_TOUCH_EXPLORATION_GESTURE_END,TYPE_WINDOW_CONTENT_CHANGED,TYPE_VIEW_SCROLLED,TYPE_VIEW_TEXT_SELECTION_CHANGED,TYPE_ANNOUNCEMENT,TYPE_VIEW_ACCESSIBILITY_FOCUSED,TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED,TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,TYPE_GESTURE_DETECTION_START,TYPE_GESTURE_DETECTION_END,TYPE_TOUCH_INTERACTION_START,TYPE_TOUCH_INTERACTION_END,TYPE_WINDOWS_CHANGED,TYPE_VIEW_CONTEXT_CLICKED,TYPE_ASSIST_READING_CONTEXT,TYPE_SPEECH_STATE_CHANGE})Retention(RetentionPolicy.SOURCE)public interface EventType {}三、实现展示当前Activity/Dialog/Window信息的悬浮窗 如上介绍的无障碍服务是能够获取到各种交互事件从onAccessibilityEvent回调中可轻松拿到交互控件的packageName和className所以基于无障碍服务能力的支持也就很容易实现悬浮展示当前Activity的功能了。
3.1 全局悬浮窗的实现
这个业务点的关键知识点是能全局悬浮且不依赖Activity类型context也就是不需要windowToken参数的window类型。
private const val TAG TopActivityEvent
class TopActivityEventWindow {companion object {SuppressLint(StaticFieldLeak)private var rootView: View? nullSuppressLint(StaticFieldLeak)private var tvContent: TextView? nullSuppressLint(StaticFieldLeak)private var window: TopActivityEventWindow? nullSuppressLint(StaticFieldLeak)fun showEvent(ctx: Context, pkgName: String?, activityClassName: String?) {if (window null) {initEventWindow(ctx)}if (!pkgName.isNullOrBlank() !activityClassName.isNullOrBlank()) {tvContent?.text $pkgName\n$activityClassName} else {Log.e(TAG, showEvent: pkgName $pkgName, activityClassName $activityClassName)}}private fun initEventWindow(ctx: Context) {rootView LayoutInflater.from(ctx).inflate(R.layout.layout_top_activity_window, null, false)val windowManager ctx.getSystemService(Context.WINDOW_SERVICE) as? WindowManagerwindowManager?.apply {val lp WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT)lp.type if (Build.VERSION.SDK_INT Build.VERSION_CODES.N) {WindowManager.LayoutParams.TYPE_TOAST} else if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) {// android31及以上 需要使用TYPE_APPLICATION_OVERLAY才能展示使用ALERT_WINDOW会报没有权限WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY} else {WindowManager.LayoutParams.TYPE_SYSTEM_ALERT}lp.flags WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLElp.gravity Gravity.TOP or Gravity.LEFTlp.format PixelFormat.TRANSLUCENTaddView(rootView, lp)}tvContent rootView?.findViewById(R.id.top_activity_window_text)window TopActivityEventWindow()}fun dismiss(ctx: Context) {if (window ! null) {val windowManager ctx.getSystemService(Context.WINDOW_SERVICE) as? WindowManagerwindowManager?.removeView(rootView)tvContent nullrootView nullwindow null}}}
}3.1 在无障碍服务的onAccessibilityEvent中调用悬浮窗的展示逻辑
override fun onAccessibilityEvent(event: AccessibilityEvent?) {Log.d(TAG, onAccessibilityEvent: event $event)if (event?.eventType AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {TopActivityEventWindow.showEvent(this.applicationContext, ${event.packageName}, ${event.className})}}3.2 实现的实际效果
捕获pixel3a-api33模拟器的Launcher展示效果 自己的demo app的主页获取效果 Dialog的捕获效果 PopupWindow获取不到待解决
四、总结 以上是自定义Android的无障碍服务的基本用法。在清单文件中注册CustomAccessibilityService在meta-datade android.accessibilityservice为key填写配置文件xml的路径关键是intent-filter节点中要写service节点要写permission然后重写onAccessiblityEvent()方法拿到交互事件、packageName、className最后只要在系统设置-无障碍打开就能很轻松实现一个自己的无障碍服务。这块定义注册Service的套路是固定的核心是理解不同属性的作用比如配置接收哪些事件类型和反馈类型指定包名的方式等其他的步骤不用纠结直接在需要时照猫画虎就好了不必花太多时间研究基础用法了。 但是无障碍服务支持的能力还远不止于此还能实现很多丰富的功能比如触发指定控件的点击从而配合语音识别实现点击、跳转等业务逻辑需要我们进一步阅读官方文档进行学习实践总结。