uiautomator简介
UiAutomator是Google提供的用来做安卓自动化测试的一个Java库,基于Accessibility服务。功能很强,可以对第三方App进行测试,获取屏幕上任意一个APP的任意一个控件属性,并对其进行任意操作,但有两个缺点:1. 测试脚本只能使用Java语言 2. 测试脚本要打包成jar或者apk包上传到设备上才能运行
uiautomator2是对uiautomator的改进,使其能够用Python编写,能够在电脑上运行的时候就控制手机,原理是在手机上运行了一个http rpc服务,将uiautomator中的功能开放出来,然后再将这些http接口封装成Python库
github地址:https://github.com/openatx/uiautomator2
uiautomator2 除了对原有的库的bug进行了修复,还增加了很多新的Feature。主要有以下部分:
- 设备和开发机可以脱离数据线,通过WiFi互联(基于atx-agent)
- 集成了openstf/minicap达到实时屏幕投频,以及实时截图
- 集成了openstf/minitouch达到精确实时控制设备
- 修复了xiaocong/uiautomator经常性退出的问题
- 代码进行了重构和精简,方便维护
- 实现了一个设备管理平台(也支持iOS) atxserver2
- 扩充了toast获取和展示的功能
安装
安装 uiautomator2
pip install --upgrade --pre uiautomator2 # 因为uiautomator2仍在开发中,你必须添加——pre来安装开发版本
测试是否安装成功 uiautomator2 --help
安装 weditor (UI Inspector)
因为uiautomator是独占资源,所以当atx运行的时候uiautomatorviewer是不能用的,为了减少atx频繁的启停,可以使用基于浏览器技术的weditor UI查看器。https://github.com/openatx/weditor
安装方法(备注: 目前最新的稳定版为 0.1.0)
pip install -U weditor
安装 daemons to a device (Optional)
电脑连接上一个手机或多个手机, 确保adb已经添加到环境变量中,执行下面的命令会自动安装本库所需要的设备端程序:uiautomator-server
、atx-agent
、openstf/minicap
、openstf/minitouch
# 初始化 所有的已经连接到电脑的设备
python -m uiautomator2 init
安装提示success
即可
UiAutomator 常用命令
命令行常用命令
-
screenshot: 截图
uiautomator2 screenshot screenshot.jpg #图片存放在当前路径
-
current: 获取当前包名和activity
$ uiautomator2 current { "package": "com.xueqiu.android", "activity": "com.xueqiu.android.stockmodule.stockdetail.StockDetailActivity" }
-
install: 安装apk
$ uiautomator2 install com.android.chrome_81.0.4044.117_404411700.apk [D 221119 15:09:15 __init__:1295] pm install -rt /data/local/tmp/_tmp.apk Installed None
官方说不能使用,但我实测可以安装成功
-
uninstall: 卸载
$ uiautomator2 uninstall <package-name> # 卸载一个包 $ uiautomator2 uninstall <package-name-1> <package-name-2> # 卸载多个包 $ uiautomator2 uninstall --all # 全部卸载
$ uiautomator2 uninstall com.android.chrome Uninstall "com.android.chrome" OK
-
stop: 停止应用
$ uiautomator2 stop com.example.app # 停止一个app $ uiautomator2 stop --all # 停止所有的app
$ uiautomator2 stop com.xueqiu.android am force-stop "com.xueqiu.android"
设置超时时间
在假设客户端退出并结束uiautomator服务之前,等待来自客户端的新命令的时间(秒)(默认为3分钟)
d.set_new_command_timeout(300) # 改为5分钟,单位为s
开启debug模式
d.debug = True
print(d.info)
15:37:11.340 $ curl -X POST -d '{"jsonrpc": "2.0", "id": "c78bf70891f2fb0b99b082a40faa6e76", "method": "deviceInfo", "params": {}}' 'http://127.0.0.1:56258/jsonrpc/0'
15:37:11.483 Response (142 ms) >>>
{"jsonrpc":"2.0","id":"c78bf70891f2fb0b99b082a40faa6e76","result":{"currentPackageName":"com.xueqiu.android","displayHeight":1872,"displayRotation":0,"displaySizeDpX":450,"displaySizeDpY":720,"displayWidth":1170,"productName":"cancro_x64","screenOn":true,"sdkInt":23,"naturalOrientation":true}}
<<< END
{'currentPackageName': 'com.xueqiu.android', 'displayHeight': 1872, 'displayRotation': 0, 'displaySizeDpX': 450, 'displaySizeDpY': 720, 'displayWidth': 1170, 'productName': 'cancro_x64', 'screenOn': True, 'sdkInt': 23, 'naturalOrientation': True}
设置Implicit wait
设置元素查找等待时间(默认20s)
d.implicitly_wait(10.0) # 也可以通过d.settings['wait_timeout'] = 10.0 修改
d(text="Settings").click() # 如果元素在10秒内没有找到,报UiObjectNotFoundError
print("wait timeout", d.implicitly_wait()) # 获取隐身等待时间
app 管理
安装APP
d.app_install('https://down11.zol.com.cn/liaotian/huoshan15.2.0w.apk') # 支持从URL或本文文件安装APK
# [D 221119 15:58:54 __init__:1295] pm install -rt /data/local/tmp/_tmp.apk
启动APP
# 默认的这种方法是先通过atx-agent解析apk包的mainActivity,然后调用am start -n $package/$activity启动
d.app_start("com.xueqiu.android")
d.app_start("com.xueqiu.android" ,stop=True) # 启动应用前停止应用
# 使用 monkey -p com.example.hello_world -c android.intent.category.LAUNCHER 1 启动
# 这种方法有个副作用,它自动会将手机的旋转锁定给关掉
d.app_start("com.example.hello_world", use_monkey=True) # start with package name
# 通过指定main activity的方式启动应用,等价于调用am start -n com.example.hello_world/.MainActivity
d.app_start("com.example.hello_world", ".MainActivity")
关闭APP
d.app_stop("com.xueqiu.android")
# 相当于`am force-stop`,因此你可能会丢失数据
d.app_clear("com.xueqiu.android")
# 相当于 `pm clear`
关闭所有正则运行的APP
d.app_stop_all() # 停止所有APP
d.app_stop_all(excludes=['com.xueqiu.android']) # 停止除了com.xueqiu.android之外的所有APP
获取APP信息
print(d.app_info('com.xueqiu.android'))
# {'packageName': 'com.xueqiu.android', 'mainActivity': 'com.xueqiu.android.common.splash.SplashActivity', 'label': '雪球股票', 'versionName': '12.18.1', 'versionCode': 280, 'size': 88270498}
img = d.app_icon("com.xueqiu.android") # 获取APP图标
img.save('xueqiu.jpg')
获取正在运行的APP
print(d.app_list_running())
# ['com.github.uiautomator', 'com.android.systemui', 'com.xueqiu.android']
等待应用程序运行
pid = d.app_wait('com.xueqiu.android') #等待APP运行,返回PID(不会自动运行APP)
if pid:
print(f"com.xueqiu.android 运行成功")
else:
print("运行失败")
d.app_wait("com.example.android", front=True) # 等待应用前台运行
d.app_wait("com.example.android", timeout=20.0) # 最长等待时间20s(默认)
上传下载文件
上传文件
d.push('xueqiu.jpg','/sdcard/') # 上传文件
d.push('xueqiu.jpg','/sdcard/xue.jpg') # 上传文件并改名
with open("xueqiu.jpg", 'rb') as f:
d.push(f, "/sdcard/") # 以文件对象方式上传
d.push("xueqiu.jpg", "/data/local/tmp/", mode=0o755) # 上传并更改文件访问模式
下载文件
d.pull('/data/local/tmp/xueqiu.mp4','xueqiu.mp4')
指定APP打开链接
d.open_url("https://www.baidu.com")
d.open_url("taobao://taobao.com") # open Taobao app
d.open_url("appname://appnamehost")
执行shell命令
d.shell("pwd", timeout=60) # 设置超时时间,默认60s
print(d.shell("pwd")) # ShellResponse(output='/\n', exit_code=0)
output = d.shell("pwd").output # 命令执行结果
exit_code = d.shell("pwd").exit_code # 命令是否正确执行
d.shell(["ls", "-l"]) # 长命令以列表方式输入
当执行需要长期输出的命令时,需加stream=True
,否则代码讲一直在执行,直到超时失败
import time
r = d.shell("logcat",stream=True)
deadline = time.time() + 10 # run maxium 10s
try:
for line in r.iter_lines(): # r.iter_lines(chunk_size=512, decode_unicode=None, delimiter=None)
if time.time() > deadline:
break
print(line.decode("utf-8"))
finally:
r.close() # 执行此命令后终止任务
Session
Session 代表一个应用程序生命周期。可以用来启动应用程序,检测应用程序崩溃
目前session方法以停用,预计在3.0版本中再启用
启动和关闭APP
sess = d.session('com.xueqiu.android') # 启动雪球APP
sess.app_stop('com.xueqiu.android') # 停止
基本操作
基本命令
print(d.info) # 设备基本信息
# {'currentPackageName': 'com.mumu.launcher', 'displayHeight': 1872, 'displayRotation': 0, 'displaySizeDpX': 450, 'displaySizeDpY': 720, 'displayWidth': 1170, 'productName': 'cancro_x64', 'screenOn': True, 'sdkInt': 23, 'naturalOrientation': True}
print(d.window_size()) # 屏幕大小
print(d.app_current()) # 当前APP基本信息 {'package': 'com.xueqiu.android', 'activity': 'com.xueqiu.android.main.view.MainActivity'}
print(d.wait_activity("com.xueqiu.android.main.view.MainActivity", timeout=10)) # 等待当前页面活动 Output: true of false
print(d.serial) # 当前设置名称 与adb devices 中设置名称一致
print(d.wlan_ip) # 获取设置IP地址
print(d.device_info) # 获取设备详细信息
d.set_clipboard('text', 'label') # 设置粘贴板内容
print(d.clipboard) # 获取粘贴板内容
屏幕操作
d.screen_on() # 打开屏幕
d.screen_off() # 关闭屏幕
print(d.info.get('screenOn')) # 获取当前屏幕状态
d.press("home") # 按home键
d.press("back") # 按返回键
d.press(0x07, 0x02) # press keycode 0x07('0') with META ALT(0x02)
d.unlock() # 解锁屏幕
d.click(100, 200) # 点击坐标点
d.double_click(100,200,0.1) # 双击默认情况下,两次点击的间隔时间为0.1s
d.long_click(100, 200, 0.5) # 长按,默认按0.5s
d.long_click(0.5, 0.5) # 长时间点击屏幕中央 注:点击、滑动、拖动操作支持百分比
d.swipe(580, 1400, 580, 400, 0.5) # 滑动, 默认滑动速度0.5s
SwipeExt 扩展功能
d.swipe_ext("right") # 手指右滑,4选1 "left", "right", "up", "down"
d.swipe_ext("right", scale=0.9) # 默认0.9, 滑动距离为屏幕宽度的90%
d.swipe_ext("right", box=(0, 0, 100, 100)) # 在 (0,0) -> (100, 100) 这个区域做滑动
# # 实践发现上滑或下滑的时候,从中点开始滑动成功率会高一些
d.swipe_ext("up", scale=0.8)
# 还可以使用Direction作为参数
from uiautomator2 import Direction
d.swipe_ext(Direction.FORWARD) # 页面下翻, 等价于 d.swipe_ext("up"), 只是更好理解
d.swipe_ext(Direction.BACKWARD) # 页面上翻
d.swipe_ext(Direction.HORIZ_FORWARD) # 页面水平右翻
d.swipe_ext(Direction.HORIZ_BACKWARD) # 页面水平左翻
d.drag(288, 880, 800, 880) # 拖动
d.drag(288, 880, 800, 880, 0.5) # 设置拖动时间0.5s ,默认0.5s
# 点击和移动
# d.swipe_points([(x0, y0), (x1, y1), (x2, y2)], 0.2) # 从点(x0, y0)到点(x1, y1)再到点(x2, y2),两点之间的时间速度是0.2秒
d.swipe_points([(300, 700), (700, 700), (700, 1400)], 0.2)
# 点击和移动
# 这个接口属于比较底层的原始接口,感觉并不完善,不过凑合能用。注:这个地方并不支持百分比
d.touch.down(10, 10) # 模拟按下
time.sleep(.01) # down 和 move 之间的延迟,自己控制
d.touch.move(15, 15) # 模拟移动
d.touch.up() # 模拟抬起
屏幕设置
# 旋转屏幕 注,我用模拟器没有测试通过
d.set_orientation('l') # or "left"
d.set_orientation("u") # or "upsidedown"
d.set_orientation("r") # or "right"
d.set_orientation("n") # or "natural"
d.freeze_rotation() # 冻结旋转
d.freeze_rotation(False) # 解除冻结
# 屏幕截图
d.screenshot("home.jpg") # 屏幕截图
# image方法保存截图
image = d.screenshot()
image.save("home.jpg")
# opencv 方法保存解脱
import cv2
image = d.screenshot(format='opencv')
cv2.imwrite('home.jpg', image)
# open 方法保存截图
imagebin = d.screenshot(format='raw')
open("some.jpg", "wb").write(imagebin)
# 获取DOM树
xml = d.dump_hierarchy()
print(xml)
d.open_notification() # 打开通知消息
d.open_quick_settings() # 打开快捷设置
选择器
选择器是一种方便的机制,可以在当前窗口中标识特定的UI对象。
演示用例:
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1170,62]">
<node index="1" text="" resource-id="com.android.systemui:id/status_bar" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1170,62]">
<node index="0" text="" resource-id="com.android.systemui:id/status_bar_contents" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1170,62]">
<node index="0" text="" resource-id="com.android.systemui:id/notification_icon_area" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[16,0][1007,62]">
<node index="0" text="" resource-id="com.android.systemui:id/notification_icon_area_inner" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[16,0][1007,62]">
<node index="0" text="" resource-id="com.android.systemui:id/notificationIcons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[16,0][946,62]">
<node index="0" text="" resource-id="" class="android.widget.ImageView" package="com.android.systemui" content-desc="UIAutomator service started" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[25,9][69,53]" />
</node>
</node>
</node>
<node index="1" text="" resource-id="com.android.systemui:id/system_icon_area" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1007,0][1149,62]">
<node index="0" text="" resource-id="com.android.systemui:id/system_icons" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1007,0][1079,62]">
<node index="1" text="" resource-id="com.android.systemui:id/signal_cluster" class="android.widget.LinearLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1014,9][1079,53]">
<node index="0" text="" resource-id="com.android.systemui:id/wifi_combo" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="WLAN 信号满格。" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1014,9][1061,53]">
<node index="1" text="" resource-id="com.android.systemui:id/wifi_signal" class="android.widget.ImageView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1014,9][1061,53]" />
</node>
</node>
</node>
<node index="1" text="1:31" resource-id="com.android.systemui:id/clock" class="android.widget.TextView" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1079,0][1149,62]" />
</node>
</node>
</node>
<node index="2" text="" resource-id="com.android.systemui:id/scrim_behind" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1170,62]" />
<node index="3" text="" resource-id="com.android.systemui:id/panel_holder" class="android.widget.FrameLayout" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1170,62]" />
<node index="4" text="" resource-id="com.android.systemui:id/scrim_in_front" class="android.view.View" package="com.android.systemui" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1170,62]" />
</node>
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1170,1872]">
<node index="0" text="" resource-id="android:id/decor_content_parent" class="android.view.ViewGroup" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1170,1872]">
<node index="0" text="" resource-id="android:id/action_bar_container" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,62][1170,208]">
<node index="0" text="" resource-id="android:id/action_bar" class="android.view.ViewGroup" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,62][1170,208]">
<node index="0" text="设置" resource-id="" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[41,100][145,170]" />
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[1045,62][1170,208]">
<node index="0" text="" resource-id="com.android.settings:id/search" class="android.widget.TextView" package="com.android.settings" content-desc="搜索设置" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[1045,72][1170,197]" />
</node>
</node>
</node>
<node index="1" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,208][1170,1872]">
<node index="0" text="" resource-id="com.android.settings:id/main_content" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,208][1170,1872]">
<node index="0" text="" resource-id="com.android.settings:id/dashboard" class="android.widget.ScrollView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,208][1170,1872]">
<node index="0" text="" resource-id="com.android.settings:id/dashboard_container" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,208][1170,1872]">
<node index="0" text="" resource-id="com.android.settings:id/category" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,208][1170,894]">
<node index="0" text="无线和网络" resource-id="com.android.settings:id/category_title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,208][1170,333]" />
<node index="1" text="" resource-id="com.android.settings:id/category_content" class="android.view.ViewGroup" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,333][1170,894]">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,333][1170,520]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,333][1170,520]">
<node index="0" text="" resource-id="com.android.settings:id/icon" class="android.widget.ImageView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,395][104,457]" />
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,333][1170,520]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,333][1170,517]">
<node index="0" text="" resource-id="" class="android.widget.RelativeLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,396][304,453]">
<node index="0" text="WLAN" resource-id="com.android.settings:id/title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,396][304,453]" />
</node>
</node>
<node index="1" text="" resource-id="com.android.settings:id/tile_divider" class="android.view.View" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,517][1170,520]" />
</node>
</node>
</node>
<node index="1" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,520][1170,707]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,520][1170,707]">
<node index="0" text="" resource-id="com.android.settings:id/icon" class="android.widget.ImageView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,582][104,644]" />
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,520][1170,707]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,520][1170,704]">
<node index="0" text="" resource-id="" class="android.widget.RelativeLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,583][271,640]">
<node index="0" text="蓝牙" resource-id="com.android.settings:id/title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,583][271,640]" />
</node>
</node>
<node index="1" text="" resource-id="com.android.settings:id/tile_divider" class="android.view.View" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,704][1170,707]" />
</node>
</node>
</node>
<node index="2" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,707][1170,894]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,707][1170,894]">
<node index="0" text="" resource-id="com.android.settings:id/icon" class="android.widget.ImageView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,769][104,831]" />
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,707][1170,894]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,707][1170,894]">
<node index="0" text="" resource-id="" class="android.widget.RelativeLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,772][271,829]">
<node index="0" text="更多" resource-id="com.android.settings:id/title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,772][271,829]" />
</node>
</node>
</node>
</node>
</node>
</node>
</node>
<node index="1" text="" resource-id="com.android.settings:id/category" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,915][1170,1872]">
<node index="0" text="设备" resource-id="com.android.settings:id/category_title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,915][1170,1040]" />
<node index="1" text="" resource-id="com.android.settings:id/category_content" class="android.view.ViewGroup" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1040][1170,1872]">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1040][1170,1227]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1040][1170,1227]">
<node index="0" text="" resource-id="com.android.settings:id/icon" class="android.widget.ImageView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,1102][104,1164]" />
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1040][1170,1227]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1040][1170,1224]">
<node index="0" text="" resource-id="" class="android.widget.RelativeLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1103][271,1160]">
<node index="0" text="显示" resource-id="com.android.settings:id/title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1103][271,1160]" />
</node>
</node>
<node index="1" text="" resource-id="com.android.settings:id/tile_divider" class="android.view.View" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1224][1170,1227]" />
</node>
</node>
</node>
<node index="1" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1227][1170,1414]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1227][1170,1414]">
<node index="0" text="" resource-id="com.android.settings:id/icon" class="android.widget.ImageView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,1289][104,1351]" />
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1227][1170,1414]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1227][1170,1411]">
<node index="0" text="" resource-id="" class="android.widget.RelativeLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1290][439,1347]">
<node index="0" text="提示音和通知" resource-id="com.android.settings:id/title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1290][439,1347]" />
</node>
</node>
<node index="1" text="" resource-id="com.android.settings:id/tile_divider" class="android.view.View" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1411][1170,1414]" />
</node>
</node>
</node>
<node index="2" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1414][1170,1601]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1414][1170,1601]">
<node index="0" text="" resource-id="com.android.settings:id/icon" class="android.widget.ImageView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,1476][104,1538]" />
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1414][1170,1601]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1414][1170,1598]">
<node index="0" text="" resource-id="" class="android.widget.RelativeLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1477][271,1534]">
<node index="0" text="应用" resource-id="com.android.settings:id/title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1477][271,1534]" />
</node>
</node>
<node index="1" text="" resource-id="com.android.settings:id/tile_divider" class="android.view.View" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1598][1170,1601]" />
</node>
</node>
</node>
<node index="3" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1601][1170,1788]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1601][1170,1788]">
<node index="0" text="" resource-id="com.android.settings:id/icon" class="android.widget.ImageView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,1663][104,1725]" />
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1601][1170,1788]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1601][1170,1785]">
<node index="0" text="" resource-id="" class="android.widget.RelativeLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1664][397,1721]">
<node index="0" text="应用兼容性" resource-id="com.android.settings:id/title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1664][397,1721]" />
</node>
</node>
<node index="1" text="" resource-id="com.android.settings:id/tile_divider" class="android.view.View" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1785][1170,1788]" />
</node>
</node>
</node>
<node index="4" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1788][1170,1872]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,1788][1170,1872]">
<node index="0" text="" resource-id="com.android.settings:id/icon" class="android.widget.ImageView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[42,1850][104,1872]" />
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1788][1170,1872]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1788][1170,1872]">
<node index="0" text="" resource-id="" class="android.widget.RelativeLayout" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1851][485,1872]">
<node index="0" text="存储设备和 USB" resource-id="com.android.settings:id/title" class="android.widget.TextView" package="com.android.settings" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[187,1851][485,1872]" />
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</hierarchy>
选择器支持以下参数。详细信息请参考 UiSelector Java 文档。
text
,textContains
,textMatches
,textStartsWith
className
,classNameMatches
description
,descriptionContains
,descriptionMatches
,descriptionStartsWith
checkable
,checked
,clickable
,longClickable
scrollable
,enabled
,focusable
,focused
,selected
packageName
,packageNameMatches
resourceId
,resourceIdMatches
index
,instance
父子节点和兄弟节点选择
父子节点
# 后去子节点或孙子节点
d(className="android.widget.RelativeLayout").child(text="蓝牙")
d(className="android.widget.FrameLayout", resourceId="android:id/content") \
.child_by_text("蓝牙", className="android.widget.TextView").click()
# 通过允许滚动搜索获取子节点
d(className="android.widget.FrameLayout", resourceId="android:id/content") \
.child_by_text(
"安全",
allow_scroll_search=True,
className="android.widget.TextView"
).click()
兄弟节点
d(text="无线和网络").sibling(className="android.view.ViewGroup")
相对定位
我们还可以使用相对定位方法来获取元素 left
, right
, top
, bottom
.
d(A).left(B)
, 在A的左侧选择Bd(A).right(B)
, 在A的右侧选择B.d(A).up(B)
, 在A的上部选择Bd(A).down(B)
, 在A是下部选择B
so:
d(text="更多").up(text="蓝牙").click()
多元素(实例)选择
有时屏幕可能包含多个具有相同属性的元素,例如文本,使用选择器中的“instance”属性来选择一个符合条件的实例
d(resourceId="com.android.settings:id/title",instance=0).click()
此外,uiautomator2提供了一个类似列表的方法:
print(d(resourceId="com.android.settings:id/title").count) # 获取元素个数,也可以用 len(d(text="Add new")) 代替
# 通过下标选择第几个元素
d(resourceId="com.android.settings:id/title")[0].click()
d(resourceId="com.android.settings:id/title")[1].click()
for view in d(resourceId="com.android.settings:id/title"):
print(view.info)
判断元素状态及信息
判断元素是否存在
print(d(text="蓝牙").exists) # True
print(d.exists(text="蓝牙")) # True 两种方法一样
print(d(text="蓝牙").exists(timeout=3)) # True 设置超时时间
获取特定UI对象的信息
print(d(text="蓝牙").info)
# {'bounds': {'bottom': 640, 'left': 187, 'right': 271, 'top': 583}, 'childCount': 0, 'className': 'android.widget.TextView', 'contentDescription': None, 'packageName': 'com.android.settings', 'resourceName': 'com.android.settings:id/title', 'text': '蓝牙', 'visibleBounds': {'bottom': 640, 'left': 187, 'right': 271, 'top': 583}, 'checkable': False, 'checked': False, 'clickable': False, 'enabled': True, 'focusable': False, 'focused': False, 'longClickable': False, 'scrollable': False, 'selected': False}
获取/设置/清除可编辑字段的文本
print(d(text="Settings").get_text()) # 获取可编辑字段值
d(text="Settings").set_text("My text...") # 设置/修改值
d(text="My text...").clear_text() # 清除值
获取元素位置
# 获取元素中心点
x, y = d(text="蓝牙").center()
print(x, y) # 239.0 135.0
x, y = d(text="蓝牙").center(offset=(0, 0)) # 左上角坐标值
print(x, y) # 187 583
# 对获取到的元素截图
im = d(text="蓝牙").screenshot()
im.save("蓝牙.jpg")
扩展选择器 XPath
Java uiautoamtor中默认是不支持xpath的,所以这里属于扩展的一个功能。速度不是这么的快
常见用法
# wait exists 10s
d.xpath("//android.widget.TextView").wait(10.0)
# find and click
d.xpath("//*[@content-desc='分享']").click()
# check exists
if d.xpath("//android.widget.TextView[contains(@text, 'Se')]").exists:
print("exists")
# get all text-view text, attrib and center point
for elem in d.xpath("//android.widget.TextView").all():
print("Text:", elem.text)
# Dictionary eg:
# {'index': '1', 'text': '999+', 'resource-id': 'com.netease.cloudmusic:id/qb', 'package': 'com.netease.cloudmusic', 'content-desc': '', 'checkable': 'false', 'checked': 'false', 'clickable': 'false', 'enabled': 'true', 'focusable': 'false', 'focused': 'false','scrollable': 'false', 'long-clickable': 'false', 'password': 'false', 'selected': 'false', 'visible-to-user': 'true', 'bounds': '[661,1444][718,1478]'}
print("Attrib:", elem.attrib)
# Coordinate eg: (100, 200)
print("Position:", elem.center())
元素操作
# 单击
d(text="蓝牙").click()
# 单击,等待超时时间为10s
d(text="蓝牙").click(timeout=10)
# 点击偏移量(x_offset, y_offset)
# click_x = x_offset * width + x_left_top
# click_y = y_offset * height + y_left_top
d(text="蓝牙").click(offset=(0.5, 0.5)) # 默认值为中间
d(text="蓝牙").click(offset=(0, 0)) # 点击左上角
d(text="蓝牙").click(offset=(1, 1)) # 点击右下角
# 单击,元素不存在不报错,返回布尔值,默认超时时间为0秒
clicked = d(text='蓝牙').click_exists(timeout=10.0)
print(clicked)
# 点击直到元素消失,返回布尔值
is_gone = d(text="蓝牙").click_gone(maxretry=10, interval=1.0) # 最大重试默认值为10,间隔默认值为1.0
# 长按
d(text="蓝牙").long_click()
针对特定UI对象的手势操作
元素拖拽
# # 拖拽特定元素到相应坐标,拖动时间0.5s
# d(text="蓝牙").drag_to(988, 610, duration=0.5)
# # 拖拽特定元素到另一个元素位置,拖拽时间0.25s
# d(text="蓝牙").drag_to(text="更多", duration=0.25)
元素移动
# # 元素滑动
# d(text="蓝牙").swipe("right") # 元素右滑
# d(text="蓝牙").swipe("left", steps=10) # 元素左滑
# d(text="蓝牙").swipe("up", steps=20) # 元素上滑滑 1 steps 大约 5ms, 因此 20 steps 大约 0.1s
# d(text="蓝牙").swipe("down", steps=20) # 元素下滑
#
# 元素手势
# 从一个点到另一个点的两点手势
# d(text="蓝牙").gesture((988, 410), (988, 910), (588, 610), (288, 310)) # 注:测试中发现 text="蓝牙" 不起作用
# d().gesture(startPoint1, startPoint2, endPoint1, endPoint2, steps)
# # 元素移动
# # 从元素的边缘到用心,移动百分之50,一定时间10ms
# d(text="无线和网络").pinch_in(percent=50, steps=10)
# # 从元素中心移到边缘
# d(text="无线和网络").pinch_out()
元素等待
# # 查找等待元素,等待超时时间为3s(默认20s),返回布尔值
# print(d(text="蓝牙").wait(timeout=3.0)) # True
# # 等待元素消失,等待时间1s
# print(d(text="蓝牙").wait_gone(timeout=1.0)) # False
在特定的ui对象上滑动页面
# # 页面滑动(手指滑动后松开)
# # 垂直向前(手指向上)滑动
# d(scrollable=True).fling()
# # # 水平向前(手指向左)滑动
# d(scrollable=True).fling.horiz.forward()
# # 垂直向后(手指像下)滑动
# d(scrollable=True).fling.vert.backward()
# # 水平滑动到最开始的地方
# d(scrollable=True).fling.horiz.toBeginning(max_swipes=1000)
# # 垂直滑动到尾部
# d(scrollable=True).fling.toEnd()
在特定的ui对象上滚动页面
# # 页面滚动(手指一直在屏幕上)
# # 垂直向前(手指向上)滚动
# d(scrollable=True).scroll(steps=10)
# # 水平向前(手指向左)滚动
# d(scrollable=True).scroll.horiz.forward(steps=100)
# # 垂直向后(手指像下)滚动
# d(scrollable=True).scroll.vert.backward()
# # 水平滚动到最开始的地方
# d(scrollable=True).scroll.horiz.toBeginning(steps=100, max_swipes=1000)
# # 垂直滚动到尾部
# d(scrollable=True).scroll.toEnd()
# # 垂直向前滚动,直到特定的UI对象出现
# d(scrollable=True).scroll.to(text="Security")
上下文监控
with d.watch_context() as ctx:
ctx.when("^立即(下载|更新)").when("取消").click() # 当同时出现 (立即安装 或 立即取消)和 取消 按钮的时候,点击取消
ctx.when("同意").click()
ctx.when("确定").click()
# 上面三行代码是立即执行完的,不会有什么等待
ctx.wait_stable() # 开启弹窗监控,并等待界面稳定(两个弹窗检查周期内没有弹窗代表稳定)
# 使用call函数来触发函数回调
# call 支持两个参数,d和el,不区分参数位置,可以不传参,如果传参变量名不能写错
# eg: 当有元素匹配仲夏之夜,点击返回按钮
ctx.when("仲夏之夜").call(lambda d: d.press("back"))
ctx.when("确定").call(lambda el: el.click())
# 其他操作
# 为了方便也可以使用代码中默认的弹窗监控逻辑
# 下面是目前内置的默认逻辑,可以加群at群主,增加新的逻辑,或者直接提pr
# when("继续使用").click()
# when("移入管控").when("取消").click()
# when("^立即(下载|更新)").when("取消").click()
# when("同意").click()
# when("^(好的|确定)").click()
with d.watch_context(builtin=True) as ctx:
# 在已有的基础上增加
ctx.when("@tb:id/jview_view").when('//*[@content-desc="图片"]').click()
# 其他脚本逻辑
全局设置
d.HTTP_TIMEOUT = 60 # 默认值60s, http默认请求超时时间
# 当设备掉线时,等待设备在线时长,仅当TMQ=true时有效,支持通过环境变量 WAIT_FOR_DEVICE_TIMEOUT 设置
d.WAIT_FOR_DEVICE_TIMEOUT = 70
其他的配置,目前已大部分集中到 d.settings 中
# 配置点击前延时0.5s,点击后延时1s
d.settings['operation_delay'] = (.5, 1)
# 修改延迟生效的方法
# 其中 double_click, long_click 都对应click
d.settings['operation_delay_methods'] = ['click', 'swipe', 'drag', 'press']
d.settings['xpath_debug'] = True # 开启xpath插件的调试日志
d.settings['wait_timeout'] = 20.0 # 默认控件等待时间(原生操作,xpath插件的等待时间)
print(d.settings)
UiAutomator中的超时设置(隐藏方法)
print( d.jsonrpc.getConfigurator() )
d.jsonrpc.setConfigurator({"waitForIdleTimeout": 100})
Input method (没测试过)
这种方法通常用于不知道控件的情况下的输入。第一步需要切换输入法,然后发送adb广播命令,具体使用方法如下
d.set_fastinput_ime(True) # 切换成FastInputIME输入法
d.send_keys("你好123abcEFG") # adb广播输入
d.clear_text() # 清除输入框所有内容(Require android-uiautomator.apk version >= 1.0.7)
d.set_fastinput_ime(False) # 切换成正常的输入法
d.send_action("search") # 模拟输入法的搜索
send_action 说明
该函数可以使用的参数有 go
search
send
next
done
previous
什么时候该使用这个函数呢?
有些时候在EditText中输入完内容之后,调用press("search")
or press("enter")
发现并没有什么反应。 这个时候就需要send_action
函数了,这里用到了只有输入法才能用的IME_ACTION_CODE
。 send_action
先broadcast
命令发送给输入法操作IME_ACTION_CODE
,由输入法完成后续跟EditText的通信。
消息提示框toast
显示toast
print(d.toast.show("Hello world")) # True
d.toast.show("Hello world", 5.0) # 显示5s,默认1s
获取 toast
# (Args)
# 5.0:最大等待超时时间。默认的10.0
# 10.0:缓存时间。如果toast已经在最近10秒内显示,则返回缓存toast。默认10.0(可能在将来更改)
# "默认消息":如果最终没有得到toast则返回。默认没有
print(d.toast.get_message(5.0, 10.0, "default message"))
一般用法
assert "Short message" in d.toast.get_message(5.0, default="")
清除缓存的toast
d.toast.reset() # Now d.toast.get_message(0) is None
视频录制
这里没有使用手机中自带的screenrecord命令,是通过获取手机图片合成视频的方法,所以需要安装一些其他的依赖,如imageio, imageio-ffmpeg, numpy等 因为有些依赖比较大,推荐使用镜像安装。直接运行下面的命令即可。
pip3 install -U "uiautomator2[image]" -i https://pypi.doubanio.com/simple
使用方法:
d.screenrecord('output.mp4')
time.sleep(10)
# or do something else
d.screenrecord.stop() # 停止录制后,output.mp4文件才能打开
图像匹配
安装依赖:
pip3 install -U "uiautomator2[image]" -i https://pypi.doubanio.com/simple
imdata = "home.jpg" # 也可以是URL, PIL.Image或OpenCV打开的图像
d.image.match(imdata)
# 匹配待查找的图片,立刻返回一个结果
# 返回一个dict, eg: {"similarity": 0.9, "point": [200, 300]}
d.image.click(imdata, timeout=20.0)
# 在20s的时间内调用match轮询查找图片,当similarity>0.9时,执行点击操作
Stop UiAutomator
停止UiAutomator守护服务
因为有atx-agent的存在,Uiautomator会被一直守护着,如果退出了就会被重新启动起来。但是Uiautomator又是霸道的,一旦它在运行,手机上的辅助功能、电脑上的uiautomatorviewer 就都不能用了,除非关掉该框架本身的uiautomator。下面就说下两种关闭方法
d.uiautomator.stop()
# d.uiautomator.start() # 启动
print(d.uiautomator.running()) # 是否在运行