前言
我們在使用 uiautomator2 做UI自動化時,有時需要進行多次滑動操作,比如我進入某個APP頁面定位某個元素,如果該元素不存在,那么我可能會一直進行上滑操作,如果定位到該元素就停止繼續滑動。
一般的情況,如果頁面是固定不變的內容,那么我們可以嘗試並計算出大概需要滑動多少次才能到該元素的區域,這個時候我們直接把滑動次數寫死就好。
另一種情況,如果頁面是經常變化的內容,這個時候我們就不知道到底需要滑動多少次,也許需要滑動到頁面底部,才能判斷該元素是否存在。
今天我們就學習下,如何在 uiautomator2 中判斷是否已滑動到頁面底部。
利用 dump_hierarchy() 獲取界面UI結構
在 uiautomator2 中,我們通過 dump_hierarchy() 方法獲取到當前界面的UI結構信息,同時該方法會把數據以 XML 形式的字符串進行返回。
import uiautomator2 as u2
d = u2.connect()
res = d.dump_hierarchy()
print(res)
返回的形式大致如下,它反應了當前頁面的所有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.sec.android.app.launcher" 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][1080,1920]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.sec.android.app.launcher" 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][1080,1920]">
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" 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][1080,1920]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/launcher" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" 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][1080,1920]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/drag_layer" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" 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][1080,1920]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/apps_view" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" 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][1080,1920]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/apps_content" class="com.android.launcher3.allapps.view.AppsPagedView" package="com.sec.android.app.launcher" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[0,0][1080,1920]">
<node index="3" text="" resource-id="" class="android.view.ViewGroup" package="com.sec.android.app.launcher" 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="[23,225][1057,1833]">
<node index="0" text="" resource-id="" class="android.view.ViewGroup" package="com.sec.android.app.launcher" 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="[23,237][1057,1833]">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" content-desc="ATX" 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="[23,237][274,549]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/iconview_imageView" class="android.widget.ImageView" package="com.sec.android.app.launcher" 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="[67,256][229,418]" />
<node index="2" text="ATX" resource-id="com.sec.android.app.launcher:id/iconview_titleView" class="android.widget.TextView" package="com.sec.android.app.launcher" 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="[114,424][183,478]" />
</node>
</node>
</node>
</node>
<node index="1" text="" resource-id="com.sec.android.app.launcher:id/apps_searchbar_container" class="android.widget.LinearLayout" package="com.sec.android.app.launcher" 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,99][1080,207]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/app_search_wrapper" class="android.widget.RelativeLayout" package="com.sec.android.app.launcher" 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="[60,99][1020,207]">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="com.sec.android.app.launcher" 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="[60,99][1020,207]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/app_search_edit_text_wrapper" class="android.widget.LinearLayout" package="com.sec.android.app.launcher" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[60,99][774,207]">
<node index="0" text=" 搜索手機" resource-id="com.sec.android.app.launcher:id/app_search_edit_text" class="android.widget.EditText" package="com.sec.android.app.launcher" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="true" password="false" selected="false" visible-to-user="true" bounds="[72,99][774,207]" />
</node>
<node index="1" text="" resource-id="com.sec.android.app.launcher:id/voice_search_button" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" 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="[774,99][894,207]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/voice_search_icon_imageview" class="android.widget.ImageButton" package="com.sec.android.app.launcher" 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="[774,99][894,207]" />
</node>
<node index="2" text="" resource-id="com.sec.android.app.launcher:id/apps_search_divider" class="android.view.View" package="com.sec.android.app.launcher" 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="[915,129][918,177]" />
<node index="3" text="" resource-id="com.sec.android.app.launcher:id/more_button_search" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" 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="[939,117][1011,189]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/more_icon_imageview" class="android.widget.ImageButton" package="com.sec.android.app.launcher" 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="[939,117][1011,189]" />
</node>
</node>
<node index="1" text="" resource-id="" class="android.widget.LinearLayout" package="com.sec.android.app.launcher" 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="[60,204][1020,207]" />
</node>
</node>
</node>
<node index="2" text="" resource-id="com.sec.android.app.launcher:id/apps_page_indicator" class="android.widget.LinearLayout" package="com.sec.android.app.launcher" content-desc="第 4 頁,共 4 頁。" 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="[424,1821][655,1872]">
<node NAF="true" index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[424,1821][475,1872]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/inactive" class="android.widget.ImageView" package="com.sec.android.app.launcher" 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="[424,1821][475,1872]" />
</node>
<node NAF="true" index="1" text="" resource-id="" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[484,1821][535,1872]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/inactive" class="android.widget.ImageView" package="com.sec.android.app.launcher" 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="[484,1821][535,1872]" />
</node>
<node NAF="true" index="2" text="" resource-id="" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[544,1821][595,1872]">
<node index="0" text="" resource-id="com.sec.android.app.launcher:id/inactive" class="android.widget.ImageView" package="com.sec.android.app.launcher" 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="[544,1821][595,1872]" />
</node>
<node NAF="true" index="3" text="" resource-id="" class="android.widget.FrameLayout" package="com.sec.android.app.launcher" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" visible-to-user="true" bounds="[604,1821][655,1872]">
<node index="1" text="" resource-id="com.sec.android.app.launcher:id/active" class="android.widget.ImageView" package="com.sec.android.app.launcher" 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="[604,1821][655,1872]" />
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</hierarchy>
判斷已經滑動到頁面底部
我們可以利用 dump_hierarchy() 獲取界面UI結構,那么需要怎么判斷當前是否已滑動到底了呢?其實很簡單,我們只需要判斷每次滑動前的UI結構信息,與滑動后的UI結構信息一致,那么就可以看作是滑動到底部了。
import uiautomator2 as u2
import time
d = u2.connect()
while True:
# 獲取每次滑動前頁面下半部分的所有元素
page_content = d.dump_hierarchy()[(len(d.dump_hierarchy()) // 2):]
d.swipe_ext("up")
time.sleep(0.5)
# 獲取每次滑動后頁面下半部分的所有元素,並與上一次滑動前的頁面元素對比,頁面元素沒有變化時跳出循環
new_page_content = d.dump_hierarchy()[(len(d.dump_hierarchy()) // 2):]
if new_page_content == page_content:
break
print("swipe end")