对应代码:base/base_page.py、utils/gesture_helper.py
说明:本节所有手势操作均来自配套代码的
BasePage和GestureHelper,代码与实际源码 1:1 对应。
移动端测试跟 Web 测试最大的区别,就是手势。
Web 上你能干的事基本就仨:点、输入、滚动。移动端不一样------滑动、拖拽、捏合、轻拂、长按,每个操作对应一个真实的用户场景。朋友圈列表要上滑刷新,地图要双指缩放,消息列表要左滑删除,这些手势测不到,移动端测试就不完整。
这一节把配套代码里所有手势操作串一遍,从 BasePage 的基础滑动到 GestureHelper 的多指触控,全部过完。
1. 滑动操作(swipe_up/down/left/right)
BasePage 里最常用的就是四个方向的滑动,代码分布在 base_page.py 第 429-543 行。
def swipe_up(self, duration: int = 1000, distance: Optional[int] = None):
size = self.driver.get_window_size()
width = size['width']
height = size['height']
start_x = width // 2
start_y = int(height * 0.8) # 从屏幕 80% 位置开始
end_y = int(height * 0.2) if distance is None else start_y - distance
end_x = start_x
self.driver.swipe(start_x, start_y, end_x, end_y, duration)
四个方向的坐标计算逻辑:
| 方向 | 起点 | 终点 | 场景 |
|---|---|---|---|
swipe_up |
屏幕正中、Y=80%处 | Y=20%处 | 列表上滑刷新 |
swipe_down |
屏幕正中、Y=20%处 | Y=80%处 | 下拉加载历史 |
swipe_left |
X=80%处、屏幕正中 | X=20%处 | 翻页/删除 |
swipe_right |
X=20%处、屏幕正中 | X=80%处 | 侧滑菜单 |
核心思路:以屏幕百分比算坐标,不写死像素值 。不同手机分辨率不一样,直接写 start_y = 800 换台设备就废了。
distance 参数可以控制滑动距离。不传时默认滑半个屏幕,传了就从起点偏移指定像素。
自定义滑动(第 525-542 行):
def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int, duration: int = 1000):
self.driver.swipe(start_x, start_y, end_x, end_y, duration)
四个方向的滑动都是调 driver.swipe(),只是算好了起止坐标。如果标准方向不够用,直接调 swipe() 传自定义坐标就行。
2. 滚动到元素(scroll_to_element)
列表页测到某个特定元素时,不能指望它一打开就在屏幕上。BasePage 第 794-822 行做了这件事:
def scroll_to_element(self, locator_type: str, locator_value: str, direction: str = "down",
max_scrolls: int = 10):
for i in range(max_scrolls):
if self.is_element_displayed(locator_type, locator_value, timeout=2):
logger.info(f"找到目标元素,滚动次数: {i+1}")
return True
if direction <span class="wx-em-red"> "down":
self.swipe_up()
else:
self.swipe_down()
time.sleep(0.5)
logger.warning(f"滚动{max_scrolls}次后仍未找到元素")
return False
逻辑就是:每次滑动前先看看目标在不在,不在就滑一屏,最多滑 10 次 。direction="down" 表示页面往下(新内容在上方,所以要上滑页面),direction="up" 反之。
使用场景:电商 App 的商品列表页,翻到底部找到"加载更多"按钮;设置页找到某个深层选项。
GestureHelper 里的 scroll_to_text(第 535-636 行)是升级版------通过文本内容找元素,Android 上优先用 UiScrollable 原生滚动 API,兜底用 W3C Actions 滚动:
# UiScrollable 方式(Android 专属,速度快且精准)
scrollable_selector = (
f'new UiScrollable('
f'new UiSelector().scrollable(true).instance(0))'
f'.scrollIntoView('
f'new UiSelector().textContains("{text}").instance(0))'
)
element = self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, scrollable_selector)
scroll_to_text 返回找到的元素,scroll_to_element 返回布尔值。前者适合找文本内容的场景,后者适合用定位器找任意元素。
3. 拖拽操作(drag_and_drop)
拖拽在 GestureHelper 中,是配套代码里最复杂的单指手势之一。
支持三种调用方式:
# 方式1:元素拖到元素
gesture.drag_and_drop(source_btn, target_area)
# 方式2:元素拖到坐标
gesture.drag_and_drop(source_btn, end_x=500, end_y=800)
# 方式3:坐标到坐标
gesture.drag_and_drop(start_x=200, start_y=300, end_x=500, end_y=800)
内部实现用了 W3C Actions API。关键逻辑:
finger = self._create_pointer_input("drag_finger")
actions = ActionChains(self.driver)
actions.w3c_actions.add_pointer_input(finger)
# 第一步:移动到起始位置并按住
finger.pointer_move(x=start_x, y=start_y)
finger.pointer_down(button=MouseButton.LEFT)
finger.pause(duration / 1000.0)
# 第二步:拆分成多步,平滑移动到目标
steps = max(1, duration // 100)
for i in range(1, steps + 1):
progress = i / steps
current_x = int(start_x + (end_x - start_x) * progress)
current_y = int(start_y + (end_y - start_y) * progress)
finger.pointer_move(x=current_x, y=current_y)
# 第三步:松开
finger.pointer_up(button=MouseButton.LEFT)
actions.perform()
两个设计细节值得说:
- 平滑拖拽:不是从 A 瞬移到 B,而是按进度拆成多步,每 100ms 一步,看起来跟真人的拖拽轨迹一样。App 端如果检测到拖拽动作太生硬(坐标突变),可能会拒掉这次操作。
- 坐标推导 :传了元素没传坐标,自动取元素中心点当坐标;传了坐标就不管元素了。
rect属性包含x、y、width、height,中心点就是(x + w/2, y + h/2)。
还有一个简化版 drag_and_drop_fast,用 ActionChains 链式调用,不做平滑步进,适合不需要精细控制的简单拖拽场景。
4. 双指缩放(pinch_in/pinch_out)
双指缩放是移动端独有的操作。地图要放大缩小、图片要缩放查看,单指做不到------必须模拟两根手指同时触控。
GestureHelper 实现了两个方法:
pinch_in:两指向内捏合,缩小画面pinch_out:两指向外扩张,放大画面
以 pinch_in 为例:
# 创建两根手指
finger1 = self._create_pointer_input("pinch_finger_1")
finger2 = self._create_pointer_input("pinch_finger_2")
# 手指1:从中心左上方 → 中心
f1_start_x = center_x - offset
f1_start_y = center_y - offset
f1_end_x = center_x
f1_end_y = center_y
# 手指2:从中心右下方 → 中心
f2_start_x = center_x + offset
f2_start_y = center_y + offset
f2_end_x = center_x
f2_end_y = center_y
# 两根手指同时向中心移动(拆分成多步)
steps = max(1, duration // 100)
for i in range(1, steps + 1):
progress = i / steps
fx1 = int(f1_start_x + (f1_end_x - f1_start_x) * progress)
fy1 = int(f1_start_y + (f1_end_y - f1_start_y) * progress)
finger1.pointer_move(x=fx1, y=fy1)
fx2 = int(f2_start_x + (f2_end_x - f2_start_x) * progress)
fy2 = int(f2_start_y + (f2_end_y - f2_start_y) * progress)
finger2.pointer_move(x=fx2, y=fy2)
# 分别执行两根手指的动作
action1 = ActionChains(self.driver)
action1.w3c_actions.add_pointer_input(finger1)
action2 = ActionChains(self.driver)
action2.w3c_actions.add_pointer_input(finger2)
action1.perform()
action2.perform()
pinch_in 手势原理 :两根手指分别放在中心点的左上和右下两个角,同时向中心移动。offset 控制两指之间的初始距离,distance 越大,捏合幅度越大。
pinch_out 手势原理:反过来,两根手指从中心同时向两个对角方向移动。起点都在屏幕中心,终点分别是左上和右下方向。
注意这里有个重要的实现细节:W3C Actions API 不支持真正的"同时执行"多指动作 ,只能按顺序 perform------先执行 action.perform()(手指1),再执行 action2.perform()(手指2)。虽然两根手指不是严格同步的,但在 500ms 内先后执行,效果上基本等效于同时捏合。
5. 轻拂操作(flick)
轻拂(flick)跟普通滑动(swipe)的区别在于 时间更短、速度更快。普通滑动持续 1000ms,轻拂默认只有 100ms。用户场景是快速翻页、列表项左滑删除这类操作。
GestureHelper 中:
def flick(self, start_x: int = None, start_y: int = None,
end_x: int = None, end_y: int = None,
direction: str = None, distance: int = 200,
duration: int = 100):
# 支持两种模式:指定方向、指定坐标
方向模式自动计算坐标:
| 方向 | 起点 | 终点 |
|---|---|---|
up |
屏幕正中间偏下 | 向上偏移 distance |
down |
屏幕正中间偏上 | 向下偏移 distance |
left |
屏幕中偏右 | 向左偏移 distance |
right |
屏幕中偏左 | 向右偏移 distance |
跟 swipe_up/down/left/right 的区别就是 duration=100 比 duration=1000 快得多,而且滑动距离默认只有 200px 而不是半个屏幕。
元素级别轻拂 flick_element 专门做列表项滑动删除:
def flick_element(self, element, direction: str = "left", distance: int = 150,
duration: int = 100):
rect = element.rect
element_center_x = rect['x'] + rect['width'] // 2
element_center_y = rect['y'] + rect['height'] // 2
if direction </span> "left":
start_x = element_center_x + rect['width'] // 4
start_y = element_center_y
end_x = start_x - distance
end_y = element_center_y
# ... 其余方向类似
使用方式:
gesture.flick_element(list_item, direction="left") # 在列表项上左滑,露出删除按钮
起点不再是屏幕百分比,而是元素内部的某个位置(中心偏移 1/4 宽度),保证轻拂动作发生在该元素上而不是随便一个位置。
6. 长按操作(long_press)
长按在 BasePage 第 546-593 行,有两个方法。
长按元素(第 546-576 行):
def long_press(self, locator_type: str, locator_value: str, duration: int = 2000, timeout: int = 10):
element = self.find_element(locator_type, locator_value, timeout)
# 优先使用 W3C Actions API
try:
from selenium.webdriver.common.action_chains import ActionChains
actions = ActionChains(self.driver)
actions.click_and_hold(element).pause(duration / 1000).release().perform()
except Exception:
# 回退到 TouchAction
from appium.webdriver.common.touch_action import TouchAction
action = TouchAction(self.driver)
action.long_press(element, duration=duration).release().perform()
长按坐标(第 578-593 行):
def long_press_by_coordinates(self, x: int, y: int, duration: int = 2000):
from appium.webdriver.common.touch_action import TouchAction
action = TouchAction(self.driver)
action.long_press(x=x, y=y, duration=duration).release().perform()
使用场景:
- 长按桌面图标弹出快捷菜单
- 长按聊天消息复制/删除/转发
- 长按输入框粘贴最近复制的内容
注意 long_press 包含降级逻辑------优先用 W3C Actions,不行再切 TouchAction。long_press_by_coordinates 目前只用了 TouchAction,因为 click_and_hold 在坐标模式下不太好使。
7. 踩坑:TouchAction 废弃 vs W3C Actions API
这是这节最重要的部分。
Appium 1.x 时代,所有手势操作都靠 TouchAction:
from appium.webdriver.common.touch_action import TouchAction
action = TouchAction(driver)
action.press(x=100, y=200).wait(500).move_to(x=300, y=400).release().perform()
这套 API 用了好几年,简单直观。但 Appium 2.x 明确把它标为 废弃(deprecated) ,推荐全面迁移到 W3C WebDriver Actions API。
为什么废弃 :TouchAction 是 Appium 自己造的轮子,不在 W3C 标准规范里。W3C Actions API 是所有浏览器自动化工具通用的标准,Selenium 4 已经完全切换到这套 API。Appium 2.x 跟进标准,废弃了非标准的 TouchAction。
配套代码里两种方式都用着 ,但策略是优先 W3C Actions,TouchAction 只做回退:
long_press先试ActionChains.click_and_hold(),失败再切TouchAction.long_press()GestureHelper全部用 W3C Actions(PointerInput+ActionChains)swipe_up/down/left/right用driver.swipe()------这是 Appium 封装的简便方法,底层在 Appium 2.x 已切到 W3C 实现
如果你从 Appium 1.x 项目迁移上来,注意这些差异:
| 对比项 | TouchAction(废弃) | W3C Actions API |
|---|---|---|
| 导入包 | appium.webdriver.common.touch_action |
selenium.webdriver.common.actions |
| 多指支持 | MultiAction 包装 | 多 PointerInput 实例 |
| 标准程度 | Appium 私有 | W3C 行业标准 |
| 跨版本兼容 | Appium 2.x 不可靠 | Appium 2.x 原生支持 |
| 代码复杂度 | 简洁 | 稍复杂,粒度更细 |
迁移示例:
# 旧:TouchAction 方式(别用了)
action = TouchAction(driver)
action.press(el).wait(500).move_to(el2).release().perform()
# 新:W3C Actions API
from selenium.webdriver.common.action_chains import ActionChains
actions = ActionChains(driver)
actions.click_and_hold(el).pause(0.5).move_to_element(el2).release().perform()
多指触控的迁移更明显:
# 旧:MultiAction 方式
finger1 = TouchAction(driver).press(x=100, y=200)
finger2 = TouchAction(driver).press(x=300, y=400)
multi = MultiAction(driver)
multi.add(finger1, finger2)
multi.perform()
# 新:两个 PointerInput + 两个 ActionChains
finger1 = PointerInput(interaction.POINTER_TOUCH, "finger1")
finger2 = PointerInput(interaction.POINTER_TOUCH, "finger2")
action1 = ActionChains(driver)
action1.w3c_actions.add_pointer_input(finger1)
action2 = ActionChains(driver)
action2.w3c_actions.add_pointer_input(finger2)
# ... 分别构建动作序列
action1.perform()
action2.perform()
配套代码的 GestureHelper 全部用新写法,如果想参考多指触控的实现,直接看 pinch_in 和 pinch_out 两个方法就行。
总结
六个手势操作,三个在 BasePage,三个在 GestureHelper:
| 操作 | 所在文件 | 核心方法 | 场景 |
|---|---|---|---|
| 滑动 | BasePage |
swipe_up/down/left/right, swipe |
列表滚动、页面翻页 |
| 滚动到元素 | BasePage |
scroll_to_element |
列表中找到特定元素 |
| 拖拽 | GestureHelper |
drag_and_drop |
图标排序、拼图游戏 |
| 缩放 | GestureHelper |
pinch_in, pinch_out |
地图放大缩小、图片缩放 |
| 轻拂 | GestureHelper |
flick, flick_element |
快速翻页、左滑删除 |
| 长按 | BasePage |
long_press, long_press_by_coordinates |
弹出菜单、快捷操作 |
关键规则:
- W3C Actions API 是现在和未来,新代码全部用它
- 坐标用百分比算,不写死像素值------不同分辨率设备差异巨大
- 复杂手势拆多步实现平滑移动------App 端会拒掉生硬的瞬间位移
- 降级兜底------W3C Actions 不行就切 TouchAction,保证兼容性