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

如何通过拖动手势实现视图绕屏幕中心点移动旋转(限最大60度)

实现拖动手势控制视图围绕屏幕中心3D变换

需求说明

实现拖动手势交互,控制视图围绕屏幕中心点同时完成移动与旋转效果,预期效果参考:
3D交互效果示例
要求:

  • 视图旋转最大角度不超过60度
  • 所有3D变换始终围绕屏幕中心点生效

原有问题代码

@objc func circleMoved(_ gesture: UIPanGestureRecognizer) {
    switch gesture.state {
    case .changed:
        let translation = gesture.translation(in: view)
    
        circle.layer.zPosition = 200
        let spread = view.width
        let angle: CGFloat = 100
        
        let angleMoveX = angle * translation.x / spread
        let angleMoveY = angle * translation.y / spread
        
        let angleMoveXRadians = angleMoveX * CGFloat.pi/180
        let angleMoveYRadians = angleMoveY * CGFloat.pi/180
        
        var move = CATransform3DIdentity
        move = CATransform3DRotate(move, angleMoveXRadians, 0, 1, 0)
        move = CATransform3DRotate(move, -angleMoveYRadians, 1, 0, 0)
        move = CATransform3DTranslate(move, translation.x, translation.y, 0)
        
        circle.layer.transform = CATransform3DConcat(circle.layer.transform, move)
    
    
        gesture.setTranslation(.zero, in: view)  
    } 
}

原有代码问题点

  • 角度基数设置为100度,且没有做角度范围钳制,多次拖动增量叠加后旋转角度很容易超出预期
  • 没有处理手势开始的初始状态,每次都在当前transform基础上增量拼接,容易产生累计误差
  • 没有设置3D透视参数,变换顺序错误,且默认旋转锚点是视图自身中心点,无法保证围绕屏幕中心旋转
  • 没有处理手势结束、取消的分支状态,逻辑不完整

修正后实现

首先在控制器中添加几个存储属性,用来记录手势过程中的状态:

// 记录手势开始时的初始transform
private var initialTransform: CATransform3D = CATransform3DIdentity
// 记录X/Y轴累计旋转角度
private var totalRotateX: CGFloat = 0
private var totalRotateY: CGFloat = 0
// 最大旋转角度限制
private let maxRotateLimit: CGFloat = 60

替换原有手势处理方法:

@objc func circleMoved(_ gesture: UIPanGestureRecognizer) {
    let containerView = view!
    switch gesture.state {
    case .began:
        // 手势启动时记录初始状态,避免增量累计误差
        initialTransform = circle.layer.transform
        totalRotateX = 0
        totalRotateY = 0
        
        // 打开这段代码可以让视图严格围绕屏幕物理中心旋转,注意会修改视图锚点
        /*
        let screenCenter = containerView.center
        let targetAnchor = containerView.convert(screenCenter, to: circle)
        circle.layer.anchorPoint = CGPoint(
            x: targetAnchor.x / circle.bounds.width,
            y: targetAnchor.y / circle.bounds.height
        )
        circle.layer.position = screenCenter
        */
        
    case .changed:
        let translation = gesture.translation(in: containerView)
        circle.layer.zPosition = 200
        
        let halfScreenWidth = containerView.bounds.width / 2
        // 计算角度系数:拖动到屏幕边缘时刚好达到最大旋转角度
        let angleRatio = maxRotateLimit / halfScreenWidth
        
        // 计算本次拖动对应的角度增量
        let deltaX = angleRatio * translation.x
        let deltaY = angleRatio * translation.y
        
        // 累计角度后钳制到[-60, 60]区间,避免超出角度限制
        totalRotateX = max(-maxRotateLimit, min(maxRotateLimit, totalRotateX + deltaX))
        totalRotateY = max(-maxRotateLimit, min(maxRotateLimit, totalRotateY + deltaY))
        
        // 角度转弧度
        let radianX = totalRotateX * .pi / 180
        let radianY = -totalRotateY * .pi / 180
        
        // 初始化3D变换,设置m34透视参数
        var finalTransform = CATransform3DIdentity
        finalTransform.m34 = -1.0 / 500 // 透视强度可根据视觉效果调整
        // 先执行平移,再执行旋转
        finalTransform = CATransform3DTranslate(finalTransform, translation.x, translation.y, 0)
        finalTransform = CATransform3DRotate(finalTransform, radianX, 0, 1, 0)
        finalTransform = CATransform3DRotate(finalTransform, radianY, 1, 0, 0)
        
        // 基于初始状态拼接变换,消除累计误差
        circle.layer.transform = CATransform3DConcat(initialTransform, finalTransform)
        
    case .ended, .cancelled, .failed:
        // 如有需要可在这里添加手势结束后的惯性滚动、复位逻辑
        break
    }
}

补充说明

  • 如果只需要视觉上围绕屏幕中心旋转,不需要修改锚点,直接使用上述代码即可;如果要求旋转轴严格对齐屏幕物理中心,打开代码中注释的锚点调整逻辑即可
  • 透视系数m34可以根据实际视觉需求调整,数值越小透视畸变效果越强
  • 角度钳制逻辑可以保证无论怎么拖动,X/Y轴的旋转角度都不会超过60度的上限

内容的提问来源于stack exchange,提问作者Doston Rustamov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 16:54:21