【Appium 系列】第10节-手势操作实战 — 滑动、拖拽、缩放与轻拂
对应代码base/base_page.py、utils/gesture_helper.py说明本节所有手势操作均来自配套代码的BasePage和GestureHelper代码与实际源码 1:1 对应。移动端测试跟 Web 测试最大的区别就是手势。Web 上你能干的事基本就仨点、输入、滚动。移动端不一样——滑动、拖拽、捏合、轻拂、长按每个操作对应一个真实的用户场景。朋友圈列表要上滑刷新地图要双指缩放消息列表要左滑删除这些手势测不到移动端测试就不完整。这一节把配套代码里所有手势操作串一遍从BasePage的基础滑动到GestureHelper的多指触控全部过完。1. 滑动操作swipe_up/down/left/rightBasePage里最常用的就是四个方向的滑动代码分布在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屏幕正中、Y80%处Y20%处列表上滑刷新swipe_down屏幕正中、Y20%处Y80%处下拉加载历史swipe_leftX80%处、屏幕正中X20%处翻页/删除swipe_rightX20%处、屏幕正中X80%处侧滑菜单核心思路以屏幕百分比算坐标不写死像素值。不同手机分辨率不一样直接写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, timeout2): logger.info(f找到目标元素滚动次数: {i1}) return True if direction span classwx-em-red down: self.swipe_up() else: self.swipe_down() time.sleep(0.5) logger.warning(f滚动{max_scrolls}次后仍未找到元素) return False逻辑就是每次滑动前先看看目标在不在不在就滑一屏最多滑 10 次。directiondown表示页面往下新内容在上方所以要上滑页面directionup反之。使用场景电商 App 的商品列表页翻到底部找到加载更多按钮设置页找到某个深层选项。GestureHelper里的scroll_to_text第 535-636 行是升级版——通过文本内容找元素Android 上优先用UiScrollable原生滚动 API兜底用 W3C Actions 滚动# UiScrollable 方式Android 专属速度快且精准 scrollable_selector ( fnew UiScrollable( fnew UiSelector().scrollable(true).instance(0)) f.scrollIntoView( fnew 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_x500, end_y800) # 方式3坐标到坐标 gesture.drag_and_drop(start_x200, start_y300, end_x500, end_y800)内部实现用了 W3C Actions API。关键逻辑finger self._create_pointer_input(drag_finger) actions ActionChains(self.driver) actions.w3c_actions.add_pointer_input(finger) # 第一步移动到起始位置并按住 finger.pointer_move(xstart_x, ystart_y) finger.pointer_down(buttonMouseButton.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(xcurrent_x, ycurrent_y) # 第三步松开 finger.pointer_up(buttonMouseButton.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(xfx1, yfy1) 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(xfx2, yfy2) # 分别执行两根手指的动作 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屏幕正中间偏下向上偏移 distancedown屏幕正中间偏上向下偏移 distanceleft屏幕中偏右向左偏移 distanceright屏幕中偏左向右偏移 distance跟swipe_up/down/left/right的区别就是duration100比duration1000快得多而且滑动距离默认只有 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, directionleft) # 在列表项上左滑露出删除按钮起点不再是屏幕百分比而是元素内部的某个位置中心偏移 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, durationduration).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(xx, yy, durationduration).release().perform()使用场景长按桌面图标弹出快捷菜单长按聊天消息复制/删除/转发长按输入框粘贴最近复制的内容注意long_press包含降级逻辑——优先用 W3C Actions不行再切 TouchAction。long_press_by_coordinates目前只用了 TouchAction因为click_and_hold在坐标模式下不太好使。7. 踩坑TouchAction 废弃 vs W3C Actions API这是这节最重要的部分。Appium 1.x 时代所有手势操作都靠TouchActionfrom appium.webdriver.common.touch_action import TouchAction action TouchAction(driver) action.press(x100, y200).wait(500).move_to(x300, y400).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 ActionsTouchAction 只做回退long_press先试ActionChains.click_and_hold()失败再切TouchAction.long_press()GestureHelper全部用 W3C ActionsPointerInputActionChainsswipe_up/down/left/right用driver.swipe()——这是 Appium 封装的简便方法底层在 Appium 2.x 已切到 W3C 实现如果你从 Appium 1.x 项目迁移上来注意这些差异对比项TouchAction废弃W3C Actions API导入包appium.webdriver.common.touch_actionselenium.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(x100, y200) finger2 TouchAction(driver).press(x300, y400) 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操作所在文件核心方法场景滑动BasePageswipe_up/down/left/right,swipe列表滚动、页面翻页滚动到元素BasePagescroll_to_element列表中找到特定元素拖拽GestureHelperdrag_and_drop图标排序、拼图游戏缩放GestureHelperpinch_in,pinch_out地图放大缩小、图片缩放轻拂GestureHelperflick,flick_element快速翻页、左滑删除长按BasePagelong_press,long_press_by_coordinates弹出菜单、快捷操作关键规则W3C Actions API 是现在和未来新代码全部用它坐标用百分比算不写死像素值——不同分辨率设备差异巨大复杂手势拆多步实现平滑移动——App 端会拒掉生硬的瞬间位移降级兜底——W3C Actions 不行就切 TouchAction保证兼容性