Android无障碍开发中的节点简单快速查找

450 阅读11分钟

背景

我们在Android编写一些自动化工具时,往往是使用无障碍开发。通过无障碍中监听节点变化和事件,然后获取活动窗口界面,通过界面去找一下特定的节点执行一些动作。例如:通过找到x的节点元素执行点击事件关闭广告。

显示

我们可以通过是使用Android Device Monitor去截取头条当前的界面,如下:

QQ20241209-222703.png

看起来好方便,左边点那个节点,右边对应显示,这样可视化的操作确实方便。比如我们想要找到北京商报节点,看右边是可以通过过ID去查找,但是实际上我们获取的节点没有打印出ID,北京商报在69行,如下:

└─ (0,0) [0] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  ├─ (0,1) [0-0] Class: androidx.viewpager.widget.ViewPager, ID: null, Text: null, desc: null
  │ ├─ (0,2) [0-0-0] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ ├─ (0,3) [0-0-0-0] Class: com.lynx.tasm.behavior.ui.text.UIText, ID: null, Text: 叙局势突变, desc: 叙局势突变
  │ │ ├─ (1,3) [0-0-0-1] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 已更新至最新, desc: 已更新至最新
  │ │ ├─ (2,3) [0-0-0-2] Class: com.lynx.tasm.behavior.ui.text.UIText, ID: null, Text: 韩国戒严风波, desc: 韩国戒严风波
  │ │ ├─ (3,3) [0-0-0-3] Class: com.lynx.tasm.behavior.ui.text.UIText, ID: null, Text: 实时快讯, desc: 实时快讯
  │ │ ├─ (4,3) [0-0-0-4] Class: com.lynx.tasm.behavior.ui.text.UIText, ID: null, Text: 实时更新, desc: 实时更新
  │ │ ├─ (5,3) [0-0-0-5] Class: com.lynx.tasm.behavior.ui.text.UIText, ID: null, Text: 阿萨德行踪披露, desc: 阿萨德行踪披露
  │ │ ├─ (6,3) [0-0-0-6] Class: com.lynx.tasm.behavior.ui.text.UIText, ID: null, Text: 尹锡悦被禁止离境, desc: 尹锡悦被禁止离境
  │ │ ├─ (7,3) [0-0-0-7] Class: com.lynx.tasm.behavior.ui.text.UIText, ID: null, Text: 7×24小时, desc: 7×24小时
  │ │ ├─ (8,3) [0-0-0-8] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 4, desc: 4
  │ │ ├─ (9,3) [0-0-0-9] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 传台积电2nm芯片生产良率达60%以上, desc: 传台积电2nm芯片生产良率达60%以上
  │ │ ├─ (10,3) [0-0-0-10] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 5, desc: 5
  │ │ ├─ (11,3) [0-0-0-11] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 中铁四局集团申请新专利, desc: 中铁四局集团申请新专利
  │ │ ├─ (12,3) [0-0-0-12] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 展开更多, desc: 展开更多
  │ │ ├─ (13,3) [0-0-0-13] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 游戏榜, desc: 游戏榜
  │ │ ├─ (14,3) [0-0-0-14] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: TheShy发文感谢粉丝, desc: TheShy发文感谢粉丝
  │ │ ├─ (15,3) [0-0-0-15] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 英雄联盟传奇杯S2落幕, desc: 英雄联盟传奇杯S2落幕
  │ │ ├─ (16,3) [0-0-0-16] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: TGA 2024提名作品宣传片公布, desc: TGA 2024提名作品宣传片公布
  │ │ ├─ (17,3) [0-0-0-17] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 4, desc: 4
  │ │ ├─ (18,3) [0-0-0-18] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 纯血鸿蒙版QQ飞车体验服开启预约, desc: 纯血鸿蒙版QQ飞车体验服开启预约
  │ │ ├─ (19,3) [0-0-0-19] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 5, desc: 5
  │ │ ├─ (20,3) [0-0-0-20] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 曝Switch 2将大幅升级, desc: 曝Switch 2将大幅升级
  │ │ ├─ (21,3) [0-0-0-21] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 展开更多, desc: 展开更多
  │ │ ├─ (22,3) [0-0-0-22] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 汽车榜, desc: 汽车榜
  │ │ ├─ (23,3) [0-0-0-23] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 迈腾哪款最值得买, desc: 迈腾哪款最值得买
  │ │ ├─ (24,3) [0-0-0-24] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 雷军:小米汽车12月将新增50家门店, desc: 雷军:小米汽车12月将新增50家门店
  │ │ ├─ (25,3) [0-0-0-25] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 德总理:大众汽车关闭工厂是错误, desc: 德总理:大众汽车关闭工厂是错误
  │ │ ├─ (26,3) [0-0-0-26] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 4, desc: 4
  │ │ ├─ (27,3) [0-0-0-27] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 阿维塔E16正式定名阿维塔06, desc: 阿维塔E16正式定名阿维塔06
  │ │ └─ (28,3) [0-0-0-28] Class: com.lynx.tasm.behavior.ui.text.FlattenUIText, ID: null, Text: 正在努力加载, desc: 正在努力加载
  │ ├─ (1,2) [0-0-1] Class: androidx.recyclerview.widget.RecyclerView, ID: null, Text: null, desc: null
  │ │ ├─ (0,3) [0-0-1-0] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │ ├─ (0,4) [0-0-1-0-0] Class: android.view.View, ID: null, Text: null, desc: [尬笑][尬笑][尬笑] 突然有种想学习的冲动呢??? 毕业二十...
  │ │ │ └─ (1,4) [0-0-1-0-1] Class: android.view.ViewGroup, ID: null, Text: null, desc: null
  │ │ │   ├─ (0,5) [0-0-1-0-1-0] Class: android.view.ViewGroup, ID: null, Text: null, desc: 作者,认真的宇宙JackyQi,按钮
  │ │ │   │ ├─ (0,6) [0-0-1-0-1-0-0] Class: android.widget.FrameLayout, ID: null, Text: null, desc: 头像
  │ │ │   │ └─ (1,6) [0-0-1-0-1-0-1] Class: android.widget.TextView, ID: null, Text: 认真的宇宙JackyQi, desc: null
  │ │ │   └─ (1,5) [0-0-1-0-1-1] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │     └─ (0,6) [0-0-1-0-1-1-0] Class: android.widget.Button, ID: null, Text: null, desc: 赞156
  │ │ │       └─ (0,7) [0-0-1-0-1-1-0-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 赞
  │ │ ├─ (1,3) [0-0-1-1] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │ ├─ (0,4) [0-0-1-1-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 视频
  │ │ │ ├─ (1,4) [0-0-1-1-1] Class: android.view.View, ID: null, Text: null, desc: 你敢和车模对视吗? 
  │ │ │ └─ (2,4) [0-0-1-1-2] Class: android.view.ViewGroup, ID: null, Text: null, desc: null
  │ │ │   ├─ (0,5) [0-0-1-1-2-0] Class: android.view.ViewGroup, ID: null, Text: null, desc: 作者,儿童对话加油,按钮
  │ │ │   │ ├─ (0,6) [0-0-1-1-2-0-0] Class: android.widget.FrameLayout, ID: null, Text: null, desc: 头像
  │ │ │   │ └─ (1,6) [0-0-1-1-2-0-1] Class: android.widget.TextView, ID: null, Text: 儿童对话加油, desc: null
  │ │ │   └─ (1,5) [0-0-1-1-2-1] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │     └─ (0,6) [0-0-1-1-2-1-0] Class: android.widget.Button, ID: null, Text: null, desc: 赞7876
  │ │ │       └─ (0,7) [0-0-1-1-2-1-0-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 赞
  │ │ ├─ (2,3) [0-0-1-2] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │ ├─ (0,4) [0-0-1-2-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 视频
  │ │ │ ├─ (1,4) [0-0-1-2-1] Class: android.view.View, ID: null, Text: null, desc: 这次不再帮你找借口了 
  │ │ │ └─ (2,4) [0-0-1-2-2] Class: android.view.ViewGroup, ID: null, Text: null, desc: null
  │ │ │   ├─ (0,5) [0-0-1-2-2-0] Class: android.view.ViewGroup, ID: null, Text: null, desc: 作者,CyndiAhua,按钮
  │ │ │   │ ├─ (0,6) [0-0-1-2-2-0-0] Class: android.widget.FrameLayout, ID: null, Text: null, desc: 头像
  │ │ │   │ └─ (1,6) [0-0-1-2-2-0-1] Class: android.widget.TextView, ID: null, Text: CyndiAhua, desc: null
  │ │ │   └─ (1,5) [0-0-1-2-2-1] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │     └─ (0,6) [0-0-1-2-2-1-0] Class: android.widget.Button, ID: null, Text: null, desc: 赞97
  │ │ │       └─ (0,7) [0-0-1-2-2-1-0-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 赞
  │ │ ├─ (3,3) [0-0-1-3] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │ ├─ (0,4) [0-0-1-3-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 视频
  │ │ │ ├─ (1,4) [0-0-1-3-1] Class: android.view.View, ID: null, Text: null, desc: 小杨哥被冻结51万股权 近日,张庆杨(疯狂小杨哥)...
  │ │ │ └─ (2,4) [0-0-1-3-2] Class: android.view.ViewGroup, ID: null, Text: null, desc: null
  │ │ │   ├─ (0,5) [0-0-1-3-2-0] Class: android.view.ViewGroup, ID: null, Text: null, desc: 作者,北京商报,认证用户 按钮
  │ │ │   │ ├─ (0,6) [0-0-1-3-2-0-0] Class: android.widget.FrameLayout, ID: null, Text: null, desc: 头像
  │ │ │   │ └─ (1,6) [0-0-1-3-2-0-1] Class: android.widget.TextView, ID: null, Text: 北京商报, desc: null
  │ │ │   └─ (1,5) [0-0-1-3-2-1] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │     └─ (0,6) [0-0-1-3-2-1-0] Class: android.widget.Button, ID: null, Text: null, desc: 赞29
  │ │ │       └─ (0,7) [0-0-1-3-2-1-0-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 赞
  │ │ ├─ (4,3) [0-0-1-4] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │ ├─ (0,4) [0-0-1-4-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 视频
  │ │ │ ├─ (1,4) [0-0-1-4-1] Class: android.view.View, ID: null, Text: null, desc: 俄媒:阿萨德及其家人抵达莫斯科,俄罗斯已向其提供...
  │ │ │ └─ (2,4) [0-0-1-4-2] Class: android.view.ViewGroup, ID: null, Text: null, desc: null
  │ │ │   ├─ (0,5) [0-0-1-4-2-0] Class: android.view.ViewGroup, ID: null, Text: null, desc: 作者,中媒汇,认证用户 按钮
  │ │ │   │ ├─ (0,6) [0-0-1-4-2-0-0] Class: android.widget.FrameLayout, ID: null, Text: null, desc: 头像
  │ │ │   │ └─ (1,6) [0-0-1-4-2-0-1] Class: android.widget.TextView, ID: null, Text: 中媒汇, desc: null
  │ │ │   └─ (1,5) [0-0-1-4-2-1] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │ │     └─ (0,6) [0-0-1-4-2-1-0] Class: android.widget.Button, ID: null, Text: null, desc: 赞2555
  │ │ │       └─ (0,7) [0-0-1-4-2-1-0-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 赞
  │ │ └─ (5,3) [0-0-1-5] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │   ├─ (0,4) [0-0-1-5-0] Class: android.view.View, ID: null, Text: null, desc: 大冰:从作家到直播红人,揭秘其爆火背后的原因 
  │ │   └─ (1,4) [0-0-1-5-1] Class: android.view.ViewGroup, ID: null, Text: null, desc: null
  │ │     ├─ (0,5) [0-0-1-5-1-0] Class: android.view.ViewGroup, ID: null, Text: null, desc: 作者,好奇奇好,按钮
  │ │     │ ├─ (0,6) [0-0-1-5-1-0-0] Class: android.widget.FrameLayout, ID: null, Text: null, desc: 头像
  │ │     │ └─ (1,6) [0-0-1-5-1-0-1] Class: android.widget.TextView, ID: null, Text: 好奇奇好, desc: null
  │ │     └─ (1,5) [0-0-1-5-1-1] Class: android.widget.FrameLayout, ID: null, Text: null, desc: null
  │ │       └─ (0,6) [0-0-1-5-1-1-0] Class: android.widget.Button, ID: null, Text: null, desc: 赞
  │ │         └─ (0,7) [0-0-1-5-1-1-0-0] Class: android.widget.ImageView, ID: null, Text: null, desc: 赞
  │ └─ (2,2) [0-0-2] Class: androidx.recyclerview.widget.RecyclerView, ID: null, Text: null, desc: null
  ├─ (1,1) [0-1] Class: com.bytedance.platform.raster.viewpool.cache.compat.MeasureOnceRelativeLayout2, ID: null, Text: null, desc: null
  │ └─ (0,2) [0-1-0] Class: android.widget.TextView, ID: null, Text: 小米第三款车型曝光 售价预计15万 | 深圳福田连夜拆不锈钢盲道, desc: 搜索框,小米第三款车型曝光 售价预计15万 | 深圳福田连夜拆不锈钢盲道
  ├─ (2,1) [0-2] Class: android.widget.LinearLayout, ID: null, Text: null, desc: null
  │ └─ (0,2) [0-2-0] Class: android.widget.TextView, ID: null, Text: 发布, desc: 发布,按钮
  ├─ (3,1) [0-3] Class: android.widget.LinearLayout, ID: null, Text: null, desc: null
  │ └─ (0,2) [0-3-0] Class: android.widget.TextView, ID: null, Text: 头条AI, desc: AI回答,按钮
  ├─ (4,1) [0-4] Class: android.widget.HorizontalScrollView, ID: null, Text: null, desc: null
  │ ├─ (0,2) [0-4-0] Class: android.widget.Button, ID: null, Text: null, desc: 关注
  │ ├─ (1,2) [0-4-1] Class: android.widget.Button, ID: null, Text: null, desc: 推荐
  │ ├─ (2,2) [0-4-2] Class: android.widget.Button, ID: null, Text: null, desc: 热榜
  │ ├─ (3,2) [0-4-3] Class: android.widget.Button, ID: null, Text: null, desc: 发现
  │ ├─ (4,2) [0-4-4] Class: android.widget.Button, ID: null, Text: null, desc: 深圳
  │ ├─ (5,2) [0-4-5] Class: android.widget.Button, ID: null, Text: null, desc: 兴趣
  │ ├─ (6,2) [0-4-6] Class: android.widget.Button, ID: null, Text: null, desc: 视频
  │ ├─ (7,2) [0-4-7] Class: android.widget.Button, ID: null, Text: null, desc: 畅听
  │ ├─ (8,2) [0-4-8] Class: android.widget.Button, ID: null, Text: null, desc: 图片
  │ ├─ (9,2) [0-4-9] Class: android.widget.Button, ID: null, Text: null, desc: 短剧
  │ ├─ (10,2) [0-4-10] Class: android.widget.Button, ID: null, Text: null, desc: 直播
  │ ├─ (11,2) [0-4-11] Class: android.widget.Button, ID: null, Text: null, desc: 娱乐
  │ ├─ (12,2) [0-4-12] Class: android.widget.Button, ID: null, Text: null, desc: 科技
  │ ├─ (13,2) [0-4-13] Class: android.widget.Button, ID: null, Text: null, desc: 懂车帝
  │ ├─ (14,2) [0-4-14] Class: android.widget.Button, ID: null, Text: null, desc: 财经
  │ ├─ (15,2) [0-4-15] Class: android.widget.Button, ID: null, Text: null, desc: 军事
  │ ├─ (16,2) [0-4-16] Class: android.widget.Button, ID: null, Text: null, desc: 体育
  │ ├─ (17,2) [0-4-17] Class: android.widget.Button, ID: null, Text: null, desc: 国际
  │ ├─ (18,2) [0-4-18] Class: android.widget.Button, ID: null, Text: null, desc: 健康
  │ ├─ (19,2) [0-4-19] Class: android.widget.Button, ID: null, Text: null, desc: 幸福里
  │ ├─ (20,2) [0-4-20] Class: android.widget.Button, ID: null, Text: null, desc: 国风
  │ ├─ (21,2) [0-4-21] Class: android.widget.Button, ID: null, Text: null, desc: 法律
  │ ├─ (22,2) [0-4-22] Class: android.widget.Button, ID: null, Text: null, desc: NBA
  │ ├─ (23,2) [0-4-23] Class: android.widget.Button, ID: null, Text: null, desc: 漫画
  │ ├─ (24,2) [0-4-24] Class: android.widget.Button, ID: null, Text: null, desc: 热点
  │ ├─ (25,2) [0-4-25] Class: android.widget.Button, ID: null, Text: null, desc: 小说
  │ ├─ (26,2) [0-4-26] Class: android.widget.Button, ID: null, Text: null, desc: 小视频
  │ ├─ (27,2) [0-4-27] Class: android.widget.Button, ID: null, Text: null, desc: 政法
  │ ├─ (28,2) [0-4-28] Class: android.widget.Button, ID: null, Text: null, desc: 眼界
  │ ├─ (29,2) [0-4-29] Class: android.widget.Button, ID: null, Text: null, desc: 每日必看
  │ ├─ (30,2) [0-4-30] Class: android.widget.Button, ID: null, Text: null, desc: 读书
  │ ├─ (31,2) [0-4-31] Class: android.widget.Button, ID: null, Text: null, desc: 古籍
  │ └─ (32,2) [0-4-32] Class: android.widget.FrameLayout, ID: null, Text: null, desc: 岛屿读书
  │   └─ (0,3) [0-4-32-0] Class: android.widget.TextView, ID: null, Text: 岛屿读书, desc: null
  ├─ (5,1) [0-5] Class: android.widget.ImageView, ID: null, Text: null, desc: null
  ├─ (6,1) [0-6] Class: com.bytedance.platform.raster.viewpool.cache.compat.MeasureOnceRelativeLayout2, ID: null, Text: null, desc: 听一听开关
  ├─ (7,1) [0-7] Class: com.bytedance.platform.raster.viewpool.cache.compat.MeasureOnceRelativeLayout2, ID: null, Text: null, desc: null
  │ └─ (0,2) [0-7-0] Class: android.widget.TextView, ID: null, Text: 头条, desc: null
  ├─ (8,1) [0-8] Class: com.bytedance.platform.raster.viewpool.cache.compat.MeasureOnceRelativeLayout2, ID: null, Text: null, desc: null
  │ └─ (0,2) [0-8-0] Class: android.widget.TextView, ID: null, Text: 视频, desc: null
  ├─ (9,1) [0-9] Class: com.bytedance.platform.raster.viewpool.cache.compat.MeasureOnceRelativeLayout2, ID: null, Text: null, desc: null
  │ └─ (0,2) [0-9-0] Class: android.widget.TextView, ID: null, Text: 商城, desc: null
  └─ (10,1) [0-10] Class: com.bytedance.platform.raster.viewpool.cache.compat.MeasureOnceRelativeLayout2, ID: null, Text: null, desc: null
    ├─ (0,2) [0-10-0] Class: android.widget.TextView, ID: null, Text: 我的, desc: null
    └─ (1,2) [0-10-1] Class: android.view.View, ID: null, Text: null, desc: 42条更新

按照我之前是通过可视化一个个节点展开寻找,或者通过附近有唯一的节点(ID和NAME)查找。

查找节点非常头痛的事件,我发现了一个比较好的方法,你可以看到我上面每行打印都有个()[],()里面表示第几个和第几层辅助查看的。

重点是[]里面的,每个-表示每一层,多看几个就能理解了。通过上面的理解,我们可以编写这样的查找方法,通过入参根节点和0-0-1-3-2-0-1找到北京商报。下面查找代码:


fun findNodeByPath(rootNode: AccessibilityNodeInfo?, path: String): AccessibilityNodeInfo? {
    // 检查根节点和路径是否有效
    if (rootNode == null) return null

    // 如果路径为空,可以返回根节点
    if (path.isEmpty()) return rootNode

    // 将路径字符串按 '-' 拆分为索引数字
    val indices = path.split("-").mapNotNull { it.toIntOrNull() }

    // 使用一个可变变量来遍历节点
    var currentNode: AccessibilityNodeInfo? = rootNode

    for ((i, index) in indices.withIndex()) {
        if (i == 0) {
            if (index != 0) {
                currentNode = null
                break
            }
        } else {
            if (currentNode == null || index < 0 || index >= currentNode.childCount) {
                return null // 如果索引越界或当前节点为空,返回 null
            }
            // 更新 currentNode 为当前层的子节点
            currentNode = currentNode.getChild(index)
        }
    }

    return currentNode // 返回找到的节点或 null
}

//使用
findNodeByPath(rootNode, "0-0-1-3-2-0-1")

想找那个节点,就复制节点前面[0-0-1-3-2-0-1]里面字符串,这可简单多了,而且效率也会比递归查找高。下面是递归打印所有节点信息代码:

fun printNodeTreeWithCoordinates(
    node: AccessibilityNodeInfo?,
    indent: String = "",
    path: String = "",
    x: Int = 0,
    y: Int = 0,
    isLast: Boolean = true
) {
    if (node == null) return

    // 获取当前节点的信息
    val className = node.className?.toString() ?: "null"
    val viewId = node.viewIdResourceName ?: "null"
    val text = node.text?.toString() ?: "null"
    val desc = node.contentDescription?.toString() ?: "null"

    // 构建并打印当前节点的信息,包括坐标 (x, y) 和连接符
    val coordinates = "($x,$y)" // 这里 y 可以根据层级结构需要进行调整
    val connector = if (isLast) "└─" else "├─"
    val currentPath = if (path.isEmpty()) "$x" else "$path-$x"

    // 打印节点信息
    Log.d(
        TAG,
        "$indent$connector $coordinates [${currentPath}] Class: $className, ID: $viewId, Text: $text, desc: $desc"
    )

    // 准备下一层级的前缀
    val newIndent = if (isLast) "$indent  " else "$indent│ "

    // 递归遍历子节点
    val childCount = node.childCount
    for (i in 0 until childCount) {
        val child = node.getChild(i)
        // 更新 y 值,可以简单设置 y 为当前层级的索引
        printNodeTreeWithCoordinates(
            child,
            newIndent,
            currentPath,
            i,
            y + 1,
            i == childCount - 1
        )
        // 不回收子节点,因为只是递归过程,且不影响 parent node
    }
}

//使用
printNodeTreeWithCoordinates(rootNode)