# Android 控件知識
Android 是通過容器的布局屬性來管理子控件的位置關系的,布局過程就是把界面上的所有的控件根據他們的間距的大小,擺放在正確的位置。
布局是一種可用于放置很多控件的容器,它可以按照一定的規律調整內部控件的位置,從而編寫出精美的界面。當然,布局的內部除了放置控件外,也可以放置布局,通過多層布局的嵌套,我們就能夠完成一些比較復雜的界面。
**Android 七大布局:**
* LinearLayout(線性布局)
* RelativeLayout(相對布局)
* FrameLayout(幀布局)
* AbsoluteLayout(絕對布局)
* TableLayout(表格布局)
* GridLayout(網格布局)
* ConstraintLayout(約束布局)
**Android 四大組件:**
* `activity`:與用戶交互的可視化界面。
* `service`:實現程序后臺運行的解決方案。
* `content provider`:內容提供者,提供程序所需要的數據。
* `broadcast receiver`:廣播接收器,監聽外部事件的到來(比如來電)。
**常用的控件:**
* TextView(文本控件)、EditText(可編輯文本控件)
* Button(按鈕)、ImageButtbn(圖片按鈕)、ToggleButton(開關按鈕)
* ImageView(圖片控件)
* CheckBox(復選框控件)、RadioButton(單選框控件)
**DOM:**
* `dom`:Document Object Model,文檔對象模型。
* `dom 應用`:最早應用于 html 和 js 的交互,用于表示界面的控件層級、界面的結構化描述。常見的格式為 html、xml。核心元素為節點和屬性。
* `xpath`:xml 路徑語言,用于 xml 中的節點定位。
* `Android 應用的層級結構`與 html 不一樣,是一個定制的`xml`。
* `app source`類似于 dom,表示 app 的層級,代表了界面里面所有的控件樹的結構。
* 每個控件都有它的`屬性`(resourceid、xpath、aid),沒有 css 屬性。
**App DOM 示例:**
* node
* attribute
* clickable
* content-desc
* resource-id
* text
* bounds
**IOS 與 Android 區別:**
* DOM 屬性和節點結構類似。
* 名字和屬性的命名不同。如:
* Android:resource-id;IOS:name
* Android:content-desc;IOS:accessibility-id
# DesiredCapabilities 配置
DesiredCapabilities 的作用是負責啟動服務端時的參數設置,是啟動 Session 時必須提供的。
DesiredCapabilities 本質上是 key-value 的對象,它告訴 Appium Server 這樣一些事情,比如:
* 本次測試是啟動瀏覽器還是啟動移動設備?
* 是啟動 Android 還是啟動 IOS ?
* 啟動 Android 時,App 的 package 是什么?
* 啟動 Android 時,App 的 activity 是什么?
* ...
**示例:**
* maven 依賴:
~~~xml
<dependency>
<groupId>io.appium</groupId>
<artifactId>java-client</artifactId>
<version>5.0.0-BETA3</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
~~~
* 測試代碼:
~~~java
import io.appium.java_client.android.AndroidDriver;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.remote.DesiredCapabilities;
import java.net.URL;
public class DemoTest {
private static AndroidDriver driver;
@BeforeAll
public static void setUp() throws Exception {
// 負責啟動服務端時的參數設置
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("deviceName", "3DN6T16B26001805"); // 設備序列號(通過adb獲取)
capabilities.setCapability("platformName", "Android"); // 手機操作系統
capabilities.setCapability("platformVersion", "6"); // 操作系統版本號
capabilities.setCapability("appPackage", "com.xsteach.appedu"); // APP包名
capabilities.setCapability("appActivity", "com.xsteach.appedu.StartActivity"); // APP最先啟動的Activity(窗體)
capabilities.setCapability("unicodeKeyboard", true); // 使用 Unicode 輸入法,支持中文輸入
// newCommandTimeout:默認60s,60s之內沒有給appium發請求,appium就會自動結束session連接,自動關閉app。
// 使用場景比如視頻處理上傳超過60s,或由于網絡原因,或者視頻過大,或上傳apk之后再運行測試等,app還沒啟動,任務就失敗了
capabilities.setCapability("newCommandTimeout", 6000);
// 連接appium server(需先啟動appium server)
driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);
}
@Test
public void testAppiumApi() {
}
@AfterAll
public static void tearDown() throws Exception {
driver.quit();
}
}
~~~
**更詳細的配置:**



# 元素基礎定位
需要注意的是每一種定位方式在界面上都可能存在多個屬性值相同的元素。
~~~java
// 通過元素的resource-id的值進行查找元素
findElementById(String resource-id);
// driver.findElementByAccessibilityId(String content-desc):通過元素的content-desc的值進行查找元素
AndroidElement ele = driver.findElementById(“com.zhihu.android:id/login_and_register”);
// findElementByName(String using):通過元素的text屬性值或者content-desc屬性值進行查找元素
AndroidElement ele = driver.findElementByName(“登錄或注冊”);
// findElementByClassName(String using):通過元素的class屬性值進行查找元素
AndroidElement ele = driver.findElementByClassName(“android.widget.Button”);
// findElementByXpath(String using):通過xpath表達式去定位元素
AndroidElement ele = driver.findElementByXpath("//android.widget.Button[@text=’登錄或注冊’]");
// findElement(By by):以by對象作為參數查找元素
findElement(By.id(String id));
findELement(By.name(String using));
findElement(By.classname(String using));
findElement(By.xpath(String using));
// 該方法的返回值和上述的相似,如:
AndroidElement ele=driver.findElement(By.id(“com.zhihu.android:id/login_and_register”));
// 定位多個元素時只要將 findElement 改成 findElements 就行。如下:
List eleList = driver.findElementsById("xxxxx");
// 或者
List eleList = driver.findElements(By.id("xxxxx"));
// 當獲取到多個相同元素為一個集合時,要操作其中一個,可以使用索引進行指定操作。比如要操作點擊第2個:
eleList.get(1).click();
// 當需要遍歷這個集合元素時,使用如下:
for(AndroidElement ae : eleList){
ae.click();
// 點擊后如果不在當前界面,這里需要一行“返回”操作的代碼
}
~~~
# uiautomator 定位
官方文檔:[https://developer.android.com/reference/android/support/test/uiautomator/UiSelector.html](https://developer.android.com/reference/android/support/test/uiautomator/UiSelector.html)
**優點**:xpath 定位速度慢,而 uiautomator 是 android 的工作引擎,速度快。
**缺點**:表達式書寫復雜,容易寫錯 IDE 沒有提示。
**定位方式:**
* 通過 resource-id 定位
* 通過 classname 定位
* 通過 content-desc 定位
* 通過文本定位
* 組合定位
* 通過父子關系定位(有時候不能直接定位某個元素,但它的父元素很好定位,這時候就先定位父元素,通過父元素找子元素)
* 通過兄弟關系定位(有時候父元素不好定位,但是其兄弟元素很好定位,這時候就可以通過兄弟元素,找到同一父元素下的子元素)
* 滾動查找元素
**示例**:
~~~java
import io.appium.java_client.MobileElement;
import io.appium.java_client.android.AndroidDriver;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.remote.DesiredCapabilities;
import java.net.URL;
import java.util.concurrent.TimeUnit;
public class DemoTest {
private static AndroidDriver driver;
@BeforeAll
public static void setUp() throws Exception {
// 負責啟動服務端時的參數設置
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("deviceName", "3DN6T16B26001805"); // 設備序列號(通過adb獲取)
capabilities.setCapability("platformName", "Android"); // 手機操作系統
capabilities.setCapability("platformVersion", "6"); // 操作系統版本號
capabilities.setCapability("appPackage", "com.xueqiu.android"); // APP包名
capabilities.setCapability("appActivity", ".view.WelcomeActivityAlias"); // APP最先啟動的Activity(窗體)
// 連接appium server(需先啟動appium server)
driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);
driver.manage().timeouts().implicitlyWait(100, TimeUnit.SECONDS);
}
@Test
public void testUiAutomatorSelector() {
AndroidDriver<MobileElement> driver = (AndroidDriver<MobileElement>) DemoTest.driver;
// 組合定位:resourceId+text文本
String idText = "resourceId(\"com.xueqiu.android:id/title_text\").text(\"推薦\")";
driver.findElementByAndroidUIAutomator(idText).click();
// 模糊匹配
driver.findElementByAndroidUIAutomator("resourceId(\"com.xueqiu.android:id/title_text\").textContains(\"熱門\")").click();
// 匹配開頭
driver.findElementByAndroidUIAutomator("resourceId(\"com.xueqiu.android:id/title_text\").textStartsWith(\"關\")").click();
// 使用正則匹配
driver.findElementByAndroidUIAutomator("resourceId(\"com.xueqiu.android:id/title_text\").textMatches(\"推\\w{1}\")").click();
// 父子關系定位
String son = "resourceId(\"com.xueqiu.android:id/scroll_view\").childSelector(text(\"推薦\"))";
driver.findElementByAndroidUIAutomator(son).click();
// 兄弟關系定位
String brother = "resourceId(\"com.xueqiu.android:id/tab_name\").fromParent(text(\"我的\"))";
driver.findElementByAndroidUIAutomator(brother).click();
// 實現滾動查找元素
String scrollFind = "new UiScrollable(new UiSelector().scrollable(true).instance(0))." +
"scrollIntoView(new UiSelector().text(\"7X24快訊\").instance(0));";
driver.findElementByAndroidUIAutomator(scrollFind).click();
}
@AfterAll
public static void tearDown() {
driver.quit();
}
}
~~~
# 元素等待
**三種經典等待方式:**
1. **強制等待**:sleep(不推薦)
2. **隱式等待(全局性)**
~~~java
// 設置一個超時時間,服務端 appium 會在給定的時間內,不停的查找,默認值是 0
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
~~~
3. **顯式等待(等待某個元素)**
~~~java
// 顯式等待10秒,每1秒掃描一次元素
WebDriverWait wait = new WebDriverWait(driver, 10, 1);
// 期望元素可定位后點擊
wait.until(ExpectedConditions.visibilityOfElementLocated(MobileBy.id("com.android.settings:id/title"))).click();
~~~
## 顯式等待
**顯式等待**
* 顯示等待與隱式等待相對,顯示等待必須在每個需要等待的元素前面進行聲明。
* 是針對于某個特定的元素設置的等待時間,即在設置時間內,默認每隔一段時間檢測一次當前頁面某個元素是否存在。
* 如果在規定的時間內找到了元素,則直接執行,即找到元素就執行相關操作。
* 如果超過設置時間檢測不到則拋出異常。默認檢測頻率為 0.5s,默認拋出異常為:NoSuchElementException。
* 顯示等待用到的兩個類:WebDriverWait、ExpectedConditions。
**頁面加載**
* 顯式等待可以等待動態加載的 ajax 元素,顯式等待需要使 ExpectedCondtions 來檢查條件。
* 一般頁面上元素的呈現順序為:
1. **title**(首先出現 title)
2. **dom**樹出現(presence,還不完整)
3. **css**出現(可見 visibility)
4. **js**出現(js 特效執行,可點擊 clickable)
* HTML 文檔是自上而下加載的,JS 文件加載會阻礙 HTML 內容的加載,解決方案是使用 JS 異步加載的方式來完成 JS 的加載。
* 樣式表下載完成之后會跟之前的樣式表一起進行解析,會對之前的元素重新渲染。
**WebDriverWait 用法**
* **WebDriverWait**用法:`wait=new WebDriverWait(driver, 10, 1000);`
* driver:瀏覽器驅動
* timeOutInSeconds:最長超時時間,默認以秒為單位
* sleepInMillis:檢測的間隔步長,默認為 0.5s
* WebDriverWait 的**until()**:`wait.until(ExpectedConditions.visibilityOf(home_search)).click();`
**ExpectedConditions 類**
* **presenceOfElementLocated**:判斷元素是否被加到了 DOM 里,并不代表該元素一定可見。
* `wait.until(ExpectedConditions.presenceOfElementLocated(Byid('"home_search')));`
* **visibilityOfElementLocated**:判斷某個元素是否可見(“可見”表示元素非隱藏,并且元素的寬和高都不等于 0)。
* `wait.until(ExpectedConditions.visibilityOfElementLocated(By.id("home_search"));`
## 三種等待方式總結
* **隱式等待**,盡量默認都加上,時間限定在 3-6s,不要太長,為的是所有的 find\_element 方法都有一個很好的緩沖。
* **顯式等待**,用來處理隱式等待無法解決的一些問題,比如:文件上傳(可以設置長一點)可能需要設置 20s 以上。而如果只設置隱式等待,它會在每個 find 方法都等這么長時間,一日發現沒有找到元素,就會等 20s 以后才拋出異常,影響 case 的執行效率,這時候就需要用顯式等待,顯式等待可以設置的長一點。
* **強制等待**:一般不推薦,前兩種基本能解決絕大部分問題,如果某個控件沒有任何特征,只能強制等待,這種情況比較少。
# 元素操作
## 元素常用方法
~~~java
// 元素點擊
element.click();
// 輸入內容
element.sendKeys("xxxxx");
// 清空輸入框
element.clear();
// 元素是否可見
element.isDisplayed();
// 元素是否可用
element.isEnabled();
// 元素是否可選中
element.isSelected();
// 獲取元素的文本值
String text=element.getText();
// 替換元素的文本值(可以作為輸入的另一種方式)
element.replaceValue("txt");
// 獲取元素某個屬性值(不能獲取 password、package、index、bounds 這三個屬性,“content-desc”使用 contentDescription)
element.getAttribute("text");
/*
源碼地址:https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/
text、name:返回 text
class、className:返回 class(只有 API => 18 才能支持)
resource-id、resourceld:返回 resource-id(只有 API => 18 才能支持)
content-desc、contentDescription:返回 content-desc 屬性
*/
// 獲取該元素的中心點坐標
int x = element.getCenter().getX(); // 元素中心點的 x 坐標值
int y = element.getCenter().getY(); // 元素中心點的 y 坐標值
// 獲取該元素的起始點坐標
int x = logout.getLocation().getX(); // 元素的起始 x 坐標值
int y = logout.getLocation().getX(); // 元素的起始 y 坐標值
// 獲取該元素的寬高
int width=element.getSize().width; // 元素的寬
int height=element.getSize().height; // 元素的高
// 元素滑動
element.swipe(SwipeElementDirection.UP, 20,20,500); // 向上滑動
element.swipe(SwipeElementDirection.DOWN, 20,20,500); // 向下滑動
element.swipe(SwipeElementDirection.LEFT, 20,20,500); // 向左滑動
element.swipe(SwipeElementDirection.RIGHT, 20,20,500); // 向右滑動
// 手勢滑動
driver.swipe(int startx, int starty, int xend, int yend, int duration )
// 前兩個參數是滑動起始點的坐標,中間兩個參數是滑動結束點的坐標,最后一個是持續時間
driver.swipe(300, 300, 300, 1000, 500);
// tap點擊
// 1)方法定義
driver.tap(int fingers, WebElement element, int duration);
// 第一個參數是指點擊次數,第二個是點擊對象,第三個是點擊間隔時間
driver.tap(1, element, 50); // 點擊元素element
// 2)方法定義
driver.tap(int fingers, int x, int y, int duration);
// 第一個和最后一個參數同上,中間兩個是要點擊的點的坐標
driver.tap(1, 540, 540, 50); // 點擊坐標(540, 540)
~~~
## 元素常用屬性
**獲取元素文本**
* 格式:element.`text`
**獲取元素坐標**
* 格式:element.`location`
* 結果:{'y':19, 'x':498}
**獲取元素尺寸(高和寬)**
* 格式:element.`size`
* 結果:{'width':500, 'height':22}
## 獲取元素屬性
~~~java
element.getAttribute("元素屬性"); // 基本上都來自getPageSource()中元素展示的屬性
driver.getPageSource(); // 獲取頁面源碼
~~~
# 觸屏操作(TouchAction)
**TouchAction 用法:**
[https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/touch-actions.md](https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/touch-actions.md)
**TouchAction 可用的事件:**
* Press:按下
* release:抬起、釋放
* moveTo:移動
* tap:點擊
* wait:等待
* longPress:長按
* cancel:取消
* perform:執行
~~~java
// 元素長按后釋放
TouchAction touchAction = new TouchAction(driver);
WebElement element = driver.findElementByXPath("//*[@resource-id=\"com.xueqiu.android:id/title_text\" and @text=\"推薦\"]");
touchAction.longPress(element).release().perform();
~~~
# 獲取設備相關信息
~~~java
// 獲取當前 activity(可用于斷言是否跳轉到預期的 activity)
String curActivity = driver.currentActivity();
// 獲取當前網絡狀態
driver.getNetworkConnection();
// 獲取當前context
driver.getContext();
// 獲取當前界面所有資源
driver.getPageSource();
// 獲取當前appium settings設置
driver.getSettings();
// 獲取當前所有context
driver.getContextHandles();
// 獲取當前sessionid
driver.getSessionId();
// 獲取當前設備的方向(橫屏還是豎屏)
driver.getOrientation();
// 設置當前ignoreUnimportantViews值
driver.ignoreUnimportantViews(true); // 在true和false可以隨時切換
// 獲取屏幕大小
int width = driver.manage().window().getSize().getWidth();
int height = driver.manage().window().getSize().getHeight();
~~~
# 系統相關操作
~~~java
// 截屏并保存至本地
File screen = driver.getScreenshotAs(OutputType.FILE); // 截圖
File screenFile = new File("d:\\screen.png"); // 另存為的截圖文件
try {
FileUtils.copyFile(screen, screenFile); // commons-io-2.0.1.jar中的api
} catch (IOException e) {
e.printStackTrace();
}
// 啟動其他應用,跨APP(每個用例都是單獨從首頁開始執行,因為不能確認上一個用例執行完后到底停留在哪個頁面)
driver.startActivity("appPackage", "appActivity");
driver.startActivity("appPackage", "appActivity", "appWaitActivity");
// 安裝app
driver.installApp("C:\\Users\\lixionggang\\Desktop\\xinchangtai.apk");
// 判斷應用是否已安裝
driver.isAppInstalled("package name");
// 重置app,會重置app的數據
driver.resetApp();
// 卸載app
driver.removeApp("apppackage");
// 打開通知欄
driver.openNotifications();
// 設置網絡連接
// 數字0代碼全斷開,1代表開啟飛行模式,2代表開啟wifi,4代表開啟數據流量
NetworkConnectionSetting network=new NetworkConnectionSetting(2);
driver.setNetworkConnection(network);
// 鎖屏
driver.lockScreen(2);
// 判斷是否鎖屏
driver.isLocked();
~~~
# Toast 控件
**Toast 介紹**
* Toast 指的是“簡易的消息提示框”,是為了給當前視圖顯示一個浮動的顯示塊,與 dialog 不同的是,它永遠不會獲得焦點。
* Toast 的思想:盡可能不引人注意地向用戶顯示信息。
* Toast 顯示的時間有限,Toast 會根據用戶設置的顯示時間后自動消失。
* Toast 本身是個系統級別的控件,它歸屬于系統 settings。當一個 app 發送消息的時候,不是自己造出來的這個彈框,而是發給系統,由系統統一進行彈框。這類的控件不在 app 內,因此需要特殊的控件識別方法。
**Toast 定位**
* Appium 使用 uiautomator 底層的機制來分析抓取 toast,并且把 toast 放到控件樹里面,但本身并不屬于控件。
* automationName:uiautomator2
* getPageSource 是無法找到 toast 的。
* 獲取當前界面 activity:adb shell dumpsys window lgrep mCurrent
必須使用 xpath 查找 toast,(Android)固定寫法為:`//*[@class='android.widget.Toast']`。