【Appium 系列】第10节-手势操作实战 — 滑动、拖拽、缩放与轻拂

对应代码:base/base_page.pyutils/gesture_helper.py

说明:本节所有手势操作均来自配套代码的 BasePageGestureHelper,代码与实际源码 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 属性包含 xywidthheight,中心点就是 (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=100duration=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/rightdriver.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_inpinch_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 弹出菜单、快捷操作

关键规则

  1. W3C Actions API 是现在和未来,新代码全部用它
  2. 坐标用百分比算,不写死像素值------不同分辨率设备差异巨大
  3. 复杂手势拆多步实现平滑移动------App 端会拒掉生硬的瞬间位移
  4. 降级兜底------W3C Actions 不行就切 TouchAction,保证兼容性
相关推荐
闵孚龙1 小时前
Claude Code 权限系统全解析:AI Agent 安全治理、权限模式、规则匹配、沙箱防护与企业落地实战
人工智能·安全
耕烟煮云1 小时前
一篇文章讲清大语言模型发展史
人工智能·语言模型·自然语言处理
雪度娃娃1 小时前
转向现代C++——在创建对象时注意区分()和{}
开发语言·c++
硅谷秋水1 小时前
ARIS:基于对抗性多智体协作的自主研究
人工智能·科技·机器学习·语言模型·软件工程
Wanderer X1 小时前
【代码】hot100
python
实心儿儿1 小时前
Linux —— 进程间通信 - system V进程间通信 - 共享内存(1)
linux·运维·服务器
铅笔小新z1 小时前
【C语言】数组详解
c语言·开发语言
风酥糖1 小时前
Godot游戏练习01-第34节-开始引入AI开发
人工智能·游戏·godot
闵孚龙1 小时前
Claude Code Prompt Cache 缓存中断检测系统全解析:AI Agent 上下文工程、可观测性、成本优化与性能治理
人工智能·缓存·prompt