如何通过拖动手势实现视图绕屏幕中心点移动旋转(限最大60度)
实现拖动手势控制视图围绕屏幕中心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
相关产品推荐
相关产品推荐

