UIAutomator測試框架介紹

2022-12-09 18:02:58

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-serveratx-agentopenstf/minicapopenstf/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的左側選擇B
  • d(A).right(B), 在A的右側選擇B.
  • d(A).up(B), 在A的上部選擇B
  • d(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_CODEsend_actionbroadcast命令傳送給輸入法操作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()) # 是否在執行