对应代码: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,保证兼容性