You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Compose Multiplatform iOS端detectTransformGestures中calculateZoom返回1f问题

问题描述

在Compose Multiplatform开发中,使用PointerEvent.calculateZoom()结合forEachGesture实现Canvas双指缩放功能时,Android端运行正常,但iOS端失效:双指缩放手势触发时,calculateZoom()始终返回1f。排查发现核心差异是iOS端event.changes列表长度始终为1,而Android端为2。曾尝试仅通过Centroid值计算缩放,但会限制Canvas的水平滚动能力。

代码示例:

forEachGesture {
    awaitPointerEventScope {
        awaitFirstDown(requireUnconsumed = false)
    }
    awaitPointerEventScope {
        var zoom = 1f
        var pastTouchSlop = false
        val touchSlop = viewConfiguration.touchSlop

        do {
            val event = awaitPointerEvent()
            val canceled = event.changes.any { it.isConsumed }
            if (event.changes.size == 1) break
            else if (event.changes.size == 2) {
                if (isZoomAllowed) {
                    if (!canceled) {
                        val zoomChange = event.calculateZoom()
                        if (!pastTouchSlop) {
                            zoom *= zoomChange
                            val centroidSize =
                                event.calculateCentroidSize(useCurrent = false)
                            val zoomMotion = abs(1 - zoom) * centroidSize
                            if (zoomMotion > touchSlop) pastTouchSlop = true
                        }
                        if (pastTouchSlop) {
                            if (zoomChange != 1f) onZoom(zoomChange)
                            event.changes.forEach { if (it.positionChanged()) it.consume() }
                        }
                    }
                }
            } else break
        } while (!canceled && event.changes.any { it.pressed })
    }
}
差异原因

Android和iOS的触摸事件底层处理逻辑存在差异,Compose Multiplatform的事件封装层适配方式不同:

  • Android端:双指手势过程中,系统会将两个触摸点的位置变化打包到同一个PointerEvent的changes列表中,因此changes.size为2,calculateZoom()可以通过两点的前后位置对比计算出正确的缩放值。
  • iOS端:双指操作时,系统会为每个触摸点的单独变化生成独立的PointerEvent,每次事件的changes列表仅包含单个触摸点的更新,导致calculateZoom()无法获取两个触摸点的位置对比数据,只能返回默认的1f。
修复方案

不能依赖event.changes的长度判断双指状态,需手动跟踪所有活跃触摸点的历史位置,自行计算缩放比例,同时保留滚动功能:

forEachGesture {
    awaitPointerEventScope {
        awaitFirstDown(requireUnconsumed = false)
    }
    awaitPointerEventScope {
        var pastTouchSlop = false
        val touchSlop = viewConfiguration.touchSlop
        // 跟踪活跃触摸点的ID与历史位置
        val activeTouches = mutableMapOf<PointerId, Offset>()
        var previousDistance = 0f

        do {
            val event = awaitPointerEvent()
            val canceled = event.changes.any { it.isConsumed }

            // 更新活跃触摸点的位置记录
            event.changes.forEach { change ->
                if (change.pressed) {
                    activeTouches[change.id] = change.previousPosition
                } else {
                    activeTouches.remove(change.id)
                }
            }

            if (isZoomAllowed && activeTouches.size == 2 && !canceled) {
                val touchPoints = activeTouches.values.toList()
                val currentDistance = (touchPoints[0] - touchPoints[1]).getDistance()
                
                if (!pastTouchSlop) {
                    previousDistance = currentDistance
                    // 判断是否达到触摸阈值
                    val zoomMotion = abs(currentDistance - previousDistance)
                    if (zoomMotion > touchSlop) {
                        pastTouchSlop = true
                    }
                } else {
                    // 自行计算缩放比例
                    val zoomChange = currentDistance / previousDistance
                    if (zoomChange != 1f) {
                        onZoom(zoomChange)
                        previousDistance = currentDistance
                    }
                    event.changes.forEach { if (it.positionChanged()) it.consume() }
                }
            } else if (activeTouches.size == 1) {
                // 单指操作时不拦截,保留滚动逻辑
                break
            }

        } while (!canceled && activeTouches.isNotEmpty())
    }
}

修复要点

  1. 手动跟踪触摸点:用mutableMap记录每个活跃触摸点的ID和历史位置,摆脱对单次PointerEvent的changes数量依赖。
  2. 自行计算缩放比例:通过两个触摸点的当前距离与历史距离的比值计算缩放变化,替代系统的calculateZoom()方法。
  3. 保留滚动能力:当检测到单指操作时,跳出当前手势监听,让滚动事件正常传递,避免限制Canvas的水平滚动。

内容的提问来源于stack exchange,提问作者Harsha Kiran

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 15:22:44