# 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();
}
}
~~~
**更详细的配置:**
![](https://img.kancloud.cn/19/9b/199bfd92bb5e6001111b2c9ded48e112_1417x745.png)
![](https://img.kancloud.cn/8d/0d/8d0d0d1baaa41d2b3ffc2f3674272f9f_1452x768.png)
![](https://img.kancloud.cn/a2/9f/a29fe03ee81eb623d3877bce47a64cd7_1624x764.png)
# 元素基础定位
需要注意的是每一种定位方式在界面上都可能存在多个属性值相同的元素。
~~~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']`。