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

Swift:实现视图绕一角随拖动手势旋转,释放后复位

实现视图绕指定角随拖动手势旋转并自动复位

核心实现思路

  • 用UIPanGestureRecognizer替代滑动手势,实时响应手指拖动动作
  • 以目标角为旋转锚点,通过计算触摸点与锚点的角度差,实现精准跟随旋转
  • 限制旋转范围到设定的最大角度,超出后不再继续旋转
  • 手势结束或取消时触发平滑复位动画

完整代码实现

1. UIView扩展:安全设置锚点(避免视图位移)

直接修改layer.anchorPoint会导致视图位置跳动,这个扩展会先调整视图位置再设置锚点:

extension UIView {
    func setAnchorPoint(_ anchorPoint: CGPoint) {
        var newPoint = CGPoint(x: bounds.size.width * anchorPoint.x, y: bounds.size.height * anchorPoint.y)
        var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y)
        
        newPoint = newPoint.applying(transform)
        oldPoint = oldPoint.applying(transform)
        
        var position = layer.position
        position.x -= oldPoint.x
        position.x += newPoint.x
        
        position.y -= oldPoint.y
        position.y += newPoint.y
        
        layer.position = position
        layer.anchorPoint = anchorPoint
    }
}

2. 视图控制器核心代码

class YourViewController: UIViewController {
    @IBOutlet weak var container: UIView!
    @IBOutlet weak var backgroundView: UIView!
    
    // 旋转锚点(示例为右下角:x=1, y=1)
    private let rotationAnchor = CGPoint(x: 1, y: 1)
    // 最大允许旋转角度(15°转弧度)
    private let maxRotationAngle = 15.0 * .pi / 180.0
    // 记录手势初始状态
    private var initialRotation: CGFloat = 0.0
    private var initialTouchAngle: CGFloat = 0.0
    // 触觉反馈
    private let haptic = UIImpactFeedbackGenerator(style: .medium)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupPanGesture()
        // 提前设置锚点,避免手势触发时视图跳动
        container.setAnchorPoint(rotationAnchor)
    }
}

// MARK: - 拖动手势处理
extension YourViewController {
    private func setupPanGesture() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        container.addGestureRecognizer(panGesture)
    }
    
    @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
        guard let gestureView = gesture.view else { return }
        
        switch gesture.state {
        case .began:
            // 记录初始旋转角度和触摸点相对于锚点的角度
            initialRotation = gestureView.transform.rotationAngle
            let touchPoint = gesture.location(in: view)
            let anchorInSuperview = gestureView.convert(rotationAnchor, to: view)
            initialTouchAngle = angle(from: anchorInSuperview, to: touchPoint)
            haptic.prepare()
            
        case .changed:
            // 计算当前触摸点与锚点的角度
            let touchPoint = gesture.location(in: view)
            let anchorInSuperview = gestureView.convert(rotationAnchor, to: view)
            let currentTouchAngle = angle(from: anchorInSuperview, to: touchPoint)
            
            // 计算目标旋转角度,限制在0到最大角度之间
            var targetRotation = currentTouchAngle - initialTouchAngle + initialRotation
            targetRotation = max(0.0, min(targetRotation, CGFloat(maxRotationAngle)))
            
            // 实时更新视图旋转
            gestureView.transform = CGAffineTransform(rotationAngle: targetRotation)
            
            // 到达最大角度时触发反馈和背景色变化
            if targetRotation >= CGFloat(maxRotationAngle) {
                backgroundView.backgroundColor = .systemGreen
                haptic.impactOccurred()
            } else {
                backgroundView.backgroundColor = .systemBackground
            }
            
        case .ended, .cancelled:
            // 手势结束,复位视图
            rotateContainerToInitialPosition()
            
        default:
            break
        }
    }
    
    // 计算两点之间的角度(基于x轴正方向)
    private func angle(from pointA: CGPoint, to pointB: CGPoint) -> CGFloat {
        let deltaX = pointB.x - pointA.x
        let deltaY = pointB.y - pointA.y
        return atan2(deltaY, deltaX)
    }
    
    // 复位动画
    func rotateContainerToInitialPosition() {
        UIView.animate(withDuration: 0.6) {
            self.container.transform = .identity
            self.backgroundView.backgroundColor = .systemBackground
        }
    }
}

关键细节说明

  • 锚点提前设置:在viewDidLoad中完成锚点设置,避免手势触发时视图突然位移
  • 角度计算逻辑:通过atan2计算触摸点与锚点的相对角度,确保旋转完全跟随手指移动轨迹
  • 旋转范围限制:使用max/min函数将旋转角度锁定在0到设定的最大角度之间,防止过度旋转
  • 触觉反馈优化:在手势开始前调用haptic.prepare(),确保反馈及时触发

内容的提问来源于stack exchange,提问作者Martin Lahn

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 10:40:58