**
Java-Appium封装实现滑动屏幕效果
1.在客户端自动化测试过程中常常需要滑动屏幕,常见做法是自己手动传入xy滑动屏幕,一般是通过手机屏幕的大小进行一些计算后实现上下左右滑动,在一个屏幕中有多个可滚动的view,或者需要在两个元素之间滑动、或者需要指定滑动的距离,或者等等等,这些通过手机屏幕计算封装的滑动显得比较困难,如果手动写入坐标,在下一次运行更换不同大小手机后,坐标将会出现偏差,以下内容针对这些问题进行了自封装处理,实现可以在元素内滑动,两个元素间滑动并实现可滑动次数和每一次滑动可进行自定义函数处理
2.Java版本:1.8 Appium版本:1.18.3,注意:讲解中的工程结合了springboot,如果您的项目没有使用springboot,对于如自动注入的类,您可以直接new
3.原理:在查找元素时,元素属性中包含:bounds参数,参数中分别为元素的左上xy和右下xy,通过bounds属性可计算元素的所在位置,中心坐标,长和宽的中心坐标,通过这些数据计算出上下左右滑动的xy
4.源码:
4.1 PO对象:
4.2 BO对象
4.3 建造者
package com.automated.testing.common.builder;
import com.automated.testing.po.BoundSize;
import com.automated.testing.po.Bound;
import com.automated.testing.po.BoundCenter;
import com.automated.testing.bo.BoundsBo;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.WebElement;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 元素坐标构造器
*/
public class BoundsBoBuilder {
/**
* 通过给入的元素计算出BoundsBo
*
* @param webElement 元素
* @return 计算后的BoundsBo
*/
public static BoundsBo buildElementBounds(WebElement webElement) {
Objects.requireNonNull(webElement);
String bounds = webElement.getAttribute("bounds");
return buildElementBounds(analysisBound(bounds));
}
/**
* 通过给入的两个元素,将两个元素封装为一个长/正方形,计算出这个长/正方形的BoundsBo,理论上如果是用来滑动屏幕,从左往右滑,
* 那么左边的元素应传入startWebElement,右边的元素传入endWebElement
*
* @param startWebElement 起始元素
* @param endWebElement 终点元素
* @return 返回两个元素计算后长/正方形BoundsBo
*/
public static BoundsBo buildElementBounds(WebElement startWebElement, WebElement endWebElement) {
Objects.requireNonNull(startWebElement);
Objects.requireNonNull(endWebElement);
String startBounds = startWebElement.getAttribute("bounds");
String endBounds = endWebElement.getAttribute("bounds");
BoundsBo startBoundsBo = buildElementBounds(analysisBound(startBounds));
BoundsBo endBoundsBo = buildElementBounds(analysisBound(endBounds));
// 将两个元素看做成一个方框,算出两个框左上角点和右下角点
int topLeftX = Math.min(startBoundsBo.getBound().getTopLeftX(), endBoundsBo.getBound().getTopLeftX());
int topLeftY = Math.min(startBoundsBo.getBound().getTopLeftY(), endBoundsBo.getBound().getTopLeftY());
int bottomRightX = Math.max(startBoundsBo.getBound().getBottomRightX(), endBoundsBo.getBound().getBottomRightX());
int bottomRightY = Math.max(startBoundsBo.getBound().getBottomRightY(), endBoundsBo.getBound().getBottomRightY());
Bound bound = new Bound();
bound.setTopLeftX(topLeftX);
bound.setTopLeftY(topLeftY);
bound.setBottomRightX(bottomRightX);
bound.setBottomRightY(bottomRightY);
return buildElementBounds(bound);
}
/**
* 解析指定格式的元素坐标
*
* @param bounds 左上xy和右下xy 格式“123,123,345,345” ,分隔符可使用任意不是数字的可见字符
* @return 解析后的Bound
*/
private static Bound analysisBound(String bounds) {
if (StringUtils.isBlank(bounds)) throw new NullPointerException("bounds is null");
String rex = "\\d+";
Pattern pattern = Pattern.compile(rex);
Matcher matcher = pattern.matcher(bounds);
Bound bound = new Bound();
for (int i = 0; i < 4; i++) {
if (!matcher.find()) throw new RuntimeException("get bounds fail:" + bounds);
switch (i) {
case 0:
bound.setTopLeftX(Integer.parseInt(matcher.group()));
break;
case 1:
bound.setTopLeftY(Integer.parseInt(matcher.group()));
break;
case 2:
bound.setBottomRightX(Integer.parseInt(matcher.group()));
break;
case 3:
bound.setBottomRightY(Integer.parseInt(matcher.group()));
break;
default:
throw new RuntimeException("get bounds fail:" + bounds);
}
}
return bound;
}
/**
* 解析bound,通过bound解析出BoundCenter和BoundSize
*
* @param bound 元素的原始数据
* @return 封装后的BoundsBo
*/
public static BoundsBo buildElementBounds(Bound bound) {
BoundsBo boundsBo = new BoundsBo();
boundsBo.setBound(bound);
BoundSize boundSize = analysisBoundSize(bound);
int averageX = boundSize.getWidthX() >> 1;
int averageY = boundSize.getHeightY() >> 1;
boundsBo.setBoundSize(boundSize);
BoundCenter boundCenter = new BoundCenter();
boundCenter.setLeftCenterX(bound.getTopLeftX());
boundCenter.setLeftCenterY(averageY + bound.getTopLeftY());
boundCenter.setRightCenterX(bound.getBottomRightX());
boundCenter.setRightCenterY(boundCenter.getLeftCenterY());
boundCenter.setTopCenterX(averageX + bound.getTopLeftX());
boundCenter.setTopCenterY(bound.getTopLeftY());
boundCenter.setBottomCenterX(boundCenter.getTopCenterX());
boundCenter.setBottomCenterY(bound.getBottomRightY());
boundCenter.setCentralPointX(boundCenter.getTopCenterX());
boundCenter.setCentralPointY(boundCenter.getLeftCenterY());
boundsBo.setBoundCenter(boundCenter);
return boundsBo;
}
/**
* 解析元素长和高
*
* @param bound 元素的原始数据
* @return 解析后的元素数据
*/
private static BoundSize analysisBoundSize(Bound bound) {
BoundSize boundSize = new BoundSize();
boundSize.setWidthX(Math.abs(bound.getBottomRightY() - bound.getTopLeftY()));
boundSize.setHeightY(Math.abs(bound.getBottomRightX() - bound.getTopLeftX()));
return boundSize;
}
}
package com.automated.testing.common.builder;
import com.automated.testing.po.SwipeParam;
/**
* @author zly
* @version 1.0
* @date 2021/1/27 10:23
*/
public class SwipeParamBuilder {
private SwipeParam swipeParam;
public SwipeParamBuilder() {
this.swipeParam = new SwipeParam();
}
public SwipeParamBuilder setWaitSecond(int waitSecond) {
this.swipeParam.setWaitTimeSecond(waitSecond);
return this;
}
public SwipeParamBuilder setEndOffset(double endOffset) {
this.swipeParam.setEndOffset(endOffset);
return this;
}
public SwipeParamBuilder setStartOffset(double startOffset) {
this.swipeParam.setStartOffset(startOffset);
return this;
}
public SwipeParamBuilder setCycleIndex(int cycleIndex) {
this.swipeParam.setCycleIndex(cycleIndex);
return this;
}
public SwipeParam build() {
return swipeParam;
}
}
工厂模式
4.4 服务:
package com.automated.testing.service.drives;
import com.automated.testing.bo.BoundsBo;
import java.time.Duration;
import java.util.function.BooleanSupplier;
public interface OperationService {
/**
* 滑动屏幕
*
* @param startX 起始坐标x
* @param startY 起始坐标y
* @param endX 终止坐标x
* @param endY 终止坐标y
* @param numberExecutions 执行几次
* @param wait 滑动间隔时间
* @param stopCondition 当suspensiveCondition不为空并且没有到达numberExecutions时返回true结束循环,如果达到numberExecutions时,则抛出异常
*/
void swipe(int startX, int startY, int endX, int endY, int numberExecutions, Duration wait, BooleanSupplier stopCondition);
/**
* 滑动屏幕
*
* @param startX 起始坐标x
* @param startY 起始坐标y
* @param endX 终止坐标x
* @param endY 终止坐标y
* @param numberExecutions 执行几次
* @param wait 滑动间隔时间
* @param stopCondition 当suspensiveCondition不为空并且没有到达numberExecutions时返回true结束循环,如果达到numberExecutions时,则抛出异常
*/
void swipe(int startX, int startY, int endX, int endY, int numberExecutions, Duration wait, BooleanSupplier stopCondition);
/**
* 点击屏幕
*
* @param x 点击坐标x
* @param y 点击坐标y
*/
void clickScreen(int x, int y);
/**
* 使用屏幕坐标进行滑动
*
* @param swipeTypeEnum 滑动的类型
*/
void swipe(SwipeTypeEnum swipeTypeEnum);
void swipe(SwipeTypeEnum swipeTypeEnum, BooleanSupplier booleanSupplier);
/**
* 使用提供的坐标点进行滑动操作
*
* @param boundsBo 提供的坐标
* @param swipeTypeEnum 滑动的类型
*/
void swipe(BoundsBo boundsBo, SwipeTypeEnum swipeTypeEnum);
void swipe(BoundsBo boundsBo, SwipeTypeEnum swipeTypeEnum, BooleanSupplier booleanSupplier);
void swipe(BoundsBo boundsBo, SwipeParam swipeParam, SwipeTypeEnum swipeTypeEnum, BooleanSupplier booleanSupplier);
}
package com.automated.testing.service.drives.impl;
import com.automated.testing.common.builder.BoundsBoBuilder;
import com.automated.testing.service.drives.AppDriveService;
import com.automated.testing.service.drives.OperationService;
import com.automated.testing.service.drives.SwipeTypeEnum;
import com.automated.testing.po.Bound;
import com.automated.testing.po.BoundCenter;
import com.automated.testing.bo.BoundsBo;
import io.appium.java_client.android.AndroidTouchAction;
import io.appium.java_client.touch.WaitOptions;
import io.appium.java_client.touch.offset.PointOption;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.function.BooleanSupplier;
@Service
public class OperationServiceImpl implements OperationService {
@Autowired
private AppDriveService appDriveService;
@Override
public void swipe(int startX, int startY, int endX, int endY, int numberExecutions, Duration wait, BooleanSupplier stopCondition) {
if (numberExecutions < 1) throw new IllegalArgumentException("The number of executions cannot be less than 1 ");
if (wait == null) wait = Duration.ofSeconds(0);
WaitOptions waitOptions = WaitOptions.waitOptions(wait);
PointOption<?> startPointOption = PointOption.point(startX, startY);
PointOption<?> endPointOption = PointOption.point(endX, endY);
AndroidTouchAction androidTouchAction = new AndroidTouchAction(appDriveService.getDriver());
for (int i = 0; i < numberExecutions; ++i) {
if (stopCondition != null)
try {
if (stopCondition.getAsBoolean()) return;
} catch (Exception ignored) {
}
androidTouchAction.press(startPointOption);
androidTouchAction.waitAction(waitOptions);
androidTouchAction.moveTo(endPointOption);
androidTouchAction.release();
androidTouchAction.perform();
}
if (stopCondition != null) {
if (stopCondition.getAsBoolean()) return;
throw new RuntimeException("No after the number of cycles has been reached");
}
}
@Override
public void clickScreen(int x, int y) {
AndroidTouchAction androidTouchAction = new AndroidTouchAction(appDriveService.getDriver());
androidTouchAction.tap(PointOption.point(x, y)).perform().release();
}
@Override
public void swipe(SwipeTypeEnum swipeTypeEnum) {
swipe(swipeTypeEnum, null);
}
@Override
public void swipe(SwipeTypeEnum swipeTypeEnum, BooleanSupplier booleanSupplier) {
// 0.1是给出余量
double margin = 0.1;
int topLeftX = (int) (this.appDriveService.getWidth() * margin);
int topLeftY = (int) (this.appDriveService.getHeight() * margin);
int bottomRightX = this.appDriveService.getWidth() - topLeftX;
int bottomRightY = this.appDriveService.getHeight() - topLeftY;
Bound bound = new Bound();
bound.setTopLeftX(topLeftX);
bound.setTopLeftY(topLeftY);
bound.setBottomRightX(bottomRightX);
bound.setBottomRightY(bottomRightY);
BoundsBo boundsBo = BoundsBoBuilder.buildElementBounds(bound);
swipe(boundsBo, swipeTypeEnum, booleanSupplier);
}
@Override
public void swipe(BoundsBo boundsBo, SwipeTypeEnum swipeTypeEnum) {
swipe(boundsBo, swipeTypeEnum, null);
}
@Override
public void swipe(BoundsBo boundsBo, SwipeTypeEnum swipeTypeEnum, BooleanSupplier booleanSupplier) {
swipe(boundsBo, new DefaultSwipeParamFactory().create(), swipeTypeEnum, booleanSupplier);
}
@Override
public void swipe(BoundsBo boundsBo, SwipeParam swipeParam, SwipeTypeEnum swipeTypeEnum, BooleanSupplier booleanSupplier) {
BoundCenter boundCenter = boundsBo.getBoundCenter();
double startOffset = swipeParam.getStartOffset();
double endOffset = swipeParam.getEndOffset();
int height = boundsBo.getBoundSize().getHeightY();
int width = boundsBo.getBoundSize().getWidthX();
int startX, startY, endX, endY;
switch (swipeTypeEnum) {
case BOTTOM_TO_TOP:
startX = boundCenter.getBottomCenterX();
startY = boundCenter.getBottomCenterY() + (int) -(height * startOffset);
endX = boundCenter.getTopCenterX();
endY = boundCenter.getTopCenterY() + (int) (height * endOffset);
break;
case LEFT_TO_RIGHT:
startX = boundCenter.getLeftCenterX() + (int) (width * startOffset);
startY = boundCenter.getLeftCenterY();
endX = boundCenter.getRightCenterX() + (int) -(width * endOffset);
endY = boundCenter.getRightCenterY();
break;
case RIGHT_TO_LEFT:
startX = boundCenter.getRightCenterX() + (int) -(width * startOffset);
startY = boundCenter.getRightCenterY();
endX = boundCenter.getLeftCenterX() + (int) (width * endOffset);
endY = boundCenter.getLeftCenterY();
break;
case TOP_TO_BOTTOM:
startX = boundCenter.getTopCenterX();
startY = boundCenter.getTopCenterY() + (int) (height * startOffset);
endX = boundCenter.getBottomCenterX();
endY = boundCenter.getBottomCenterY() + (int) -(height * endOffset);
break;
default:
throw new IllegalArgumentException("Unsupported type:" + swipeTypeEnum);
}
swipe(
startX
, startY
, endX
, endY
, swipeParam.getCycleIndex()
, swipeParam.getWaitTime()
, booleanSupplier
);
}
}
package com.automated.testing.service.drives;
import java.time.Duration;
public enum SwipeTypeEnum {
LEFT_TO_RIGHT,
RIGHT_TO_LEFT,
TOP_TO_BOTTOM,
BOTTOM_TO_TOP;
}
通过SwipeTypeEnum中的类型,可以实现上下左右元素间的滑动了,如果您有更好的方法,请联系作者共同探讨
4.5 使用
@Test
protected void demo() {
this.homePageService.getBoutique().click();
this.sleep();
this.operationService.swipe(BoundsBoBuilder.buildElementBounds(this.driver.findElement(this.getCAWTView())),
new SwipeParamBuilder().setCycleIndex(4).build(), SwipeTypeEnum.RIGHT_TO_LEFT, null);
}