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

SwiftUI中如何在固定框架内平滑拖动旋转后的矩形?

SwiftUI 旋转视图拖动卡顿修复:实现平滑拖动且始终覆盖固定框架

问题背景

我正在开发一个SwiftUI项目,包含以下元素:

  • 固定黄色矩形框架
  • 可拖拽、旋转的蓝色矩形图像,拖动时需始终完全覆盖固定框架

当前问题:

  • 旋转功能正常,旋转时图像能正确缩放覆盖框架
  • 拖动功能可用,但旋转状态下拖动存在卡顿:
    • 有时因旋转约束突然停止移动
    • 旋转时无法在所有方向流畅移动

目标:实现旋转状态下的平滑拖动,确保:

  • 蓝色矩形始终覆盖黄色框架
  • 移动自然无卡顿
  • 图像边缘完全覆盖框架

问题分析

原代码卡顿的核心原因:

  1. 拖动时分x、y方向单独验证位置合法性,旋转状态下两个方向的约束相互关联,单独判断会导致某一方向被拒绝时,另一方向也无法流畅移动,出现卡顿。
  2. 直接拒绝不符合条件的移动,没有计算允许的最大偏移量,导致视图突然停止。

解决方案

  1. 合并位置验证逻辑:一次性计算x和y方向的合法偏移,确保新位置满足覆盖要求。
  2. 计算允许的移动边界:通过旋转后的矩形边界,算出蓝色矩形能移动的最大范围,让拖动时始终在这个范围内平滑移动,而非直接拒绝。
  3. 优化碰撞检测:简化旋转矩形的边界计算,提升手势响应速度。

修改后的完整代码

struct RotatingDraggingRectView: View {
    @State private var rectData: RectData = RectData(
        frameSize: CGSize(width: 100, height: 160),
        framePosition: CGPoint(x: 200, y: 200),
        imageSize: CGSize(width: 200, height: 150),
        imageRotation: .zero,
        imagePosition: CGPoint(x: 200, y: 200)
    )

    var body: some View {
        VStack {
            GeometryReader { geometry in
                ZStack {
                    // 固定黄色框架
                    Rectangle()
                        .fill(Color.yellow)
                        .frame(width: rectData.frameSize.width, height: rectData.frameSize.height)
                        .position(rectData.framePosition)
                    
                    // 可旋转拖动的蓝色矩形
                    Rectangle()
                        .fill(Color.blue.opacity(0.5))
                        .frame(width: rectData.imageSize.width, height: rectData.imageSize.height)
                        .rotationEffect(rectData.imageRotation)
                        .position(rectData.imagePosition)
                }
                .background(.red)
                .frame(width: geometry.size.width, height: geometry.size.width)
                .position(CGPoint(x: geometry.size.width/2, y: geometry.size.height/2))
            }
            .gesture(dragGesture())
            
            // 旋转滑块
            Slider(value: $rectData.imageRotation.degrees, in: 0...180, step: 1)
                .padding()
                .onChange(of: rectData.imageRotation) { _ in
                    adjustRectSizeForRotation()
                }
        }
        .onAppear {
            adjustRectSizeForRotation()
        }
    }
    
    // 旋转时调整图像大小以覆盖框架
    func adjustRectSizeForRotation() {
        let angleRadians = rectData.imageRotation.radians
        let absCos = abs(cos(angleRadians))
        let absSin = abs(sin(angleRadians))

        let requiredWidth = (rectData.frameSize.width * absCos) + (rectData.frameSize.height * absSin)
        let requiredHeight = (rectData.frameSize.width * absSin) + (rectData.frameSize.height * absCos)

        // 计算等比例缩放因子,确保图像足够覆盖框架(加1.01留余量)
        let scaleFactorWidth = requiredWidth / rectData.imageSize.width
        let scaleFactorHeight = requiredHeight / rectData.imageSize.height
        let scaleFactor = max(scaleFactorWidth, scaleFactorHeight) * 1.01

        rectData.imageSize.width *= scaleFactor
        rectData.imageSize.height *= scaleFactor
        rectData.imagePosition = rectData.framePosition
    }
    
    // 优化后的拖动手势
    func dragGesture() -> some Gesture {
        DragGesture()
            .onChanged { value in
                // 计算当前拖动的总偏移
                let proposedPosition = CGPoint(
                    x: rectData.imagePosition.x + value.translation.width,
                    y: rectData.imagePosition.y + value.translation.height
                )
                
                // 计算合法的最终位置
                let validPosition = calculateValidPosition(proposedPosition: proposedPosition)
                rectData.imagePosition = validPosition
            }
    }
    
    // 计算符合覆盖要求的合法位置
    private func calculateValidPosition(proposedPosition: CGPoint) -> CGPoint {
        // 获取旋转后蓝色矩形的边界
        let rotatedImageBounds = getRotatedRectBounds(
            size: rectData.imageSize,
            position: proposedPosition,
            rotation: rectData.imageRotation
        )
        // 黄色框架的边界
        let frameBounds = CGRect(
            x: rectData.framePosition.x - rectData.frameSize.width/2,
            y: rectData.framePosition.y - rectData.frameSize.height/2,
            width: rectData.frameSize.width,
            height: rectData.frameSize.height
        )
        
        // 计算蓝色矩形需要调整的偏移量,确保黄色框架完全在内部
        var adjustedX = proposedPosition.x
        var adjustedY = proposedPosition.y
        
        // 左边界:蓝色矩形左边缘不能超过黄色框架左边缘
        if rotatedImageBounds.minX > frameBounds.minX {
            adjustedX -= (rotatedImageBounds.minX - frameBounds.minX)
        }
        // 右边界:蓝色矩形右边缘不能小于黄色框架右边缘
        if rotatedImageBounds.maxX < frameBounds.maxX {
            adjustedX += (frameBounds.maxX - rotatedImageBounds.maxX)
        }
        // 上边界:蓝色矩形上边缘不能超过黄色框架上边缘
        if rotatedImageBounds.minY > frameBounds.minY {
            adjustedY -= (rotatedImageBounds.minY - frameBounds.minY)
        }
        // 下边界:蓝色矩形下边缘不能小于黄色框架下边缘
        if rotatedImageBounds.maxY < frameBounds.maxY {
            adjustedY += (frameBounds.maxY - rotatedImageBounds.maxY)
        }
        
        return CGPoint(x: adjustedX, y: adjustedY)
    }
    
    // 获取旋转后矩形的边界(最小/最大x、y)
    private func getRotatedRectBounds(size: CGSize, position: CGPoint, rotation: Angle) -> CGRect {
        let corners = getRotatedCorners(size: size, position: position, rotation: rotation)
        let minX = corners.map { $0.x }.min()!
        let maxX = corners.map { $0.x }.max()!
        let minY = corners.map { $0.y }.min()!
        let maxY = corners.map { $0.y }.max()!
        return CGRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY)
    }
    
    // 获取矩形的四个角点
    private func getCorners(size: CGSize, position: CGPoint) -> [CGPoint] {
        let halfWidth = size.width / 2
        let halfHeight = size.height / 2
        return [
            CGPoint(x: position.x - halfWidth, y: position.y - halfHeight),
            CGPoint(x: position.x + halfWidth, y: position.y - halfHeight),
            CGPoint(x: position.x + halfWidth, y: position.y + halfHeight),
            CGPoint(x: position.x - halfWidth, y: position.y + halfHeight)
        ]
    }
    
    // 获取旋转后矩形的四个角点
    private func getRotatedCorners(size: CGSize, position: CGPoint, rotation: Angle) -> [CGPoint] {
        let halfWidth = size.width / 2
        let halfHeight = size.height / 2
        let angleRadians = rotation.radians
        let cosTheta = cos(angleRadians)
        let sinTheta = sin(angleRadians)

        let localCorners = [
            CGPoint(x: -halfWidth, y: -halfHeight),
            CGPoint(x: halfWidth, y: -halfHeight),
            CGPoint(x: halfWidth, y: halfHeight),
            CGPoint(x: -halfWidth, y: halfHeight)
        ]

        return localCorners.map { corner in
            let rotatedX = corner.x * cosTheta - corner.y * sinTheta
            let rotatedY = corner.x * sinTheta + corner.y * cosTheta
            return CGPoint(x: rotatedX + position.x, y: rotatedY + position.y)
        }
    }
}

// 数据模型(修正命名规范)
struct RectData {
    var frameSize: CGSize
    var framePosition: CGPoint
    var imageSize: CGSize
    var imageRotation: Angle
    var imagePosition: CGPoint
}

// 预览
struct RotatingDraggingRectView_Previews: PreviewProvider {
    static var previews: some View {
        RotatingDraggingRectView()
    }
}

关键修改说明

  1. 拖动手势逻辑重构:不再分x、y方向单独判断,直接计算提议位置后调整为合法位置,确保移动流畅。
  2. 新增边界计算函数:getRotatedRectBounds获取旋转后矩形的边界,简化碰撞检测逻辑。
  3. 合法位置计算:calculateValidPosition通过对比蓝色矩形和黄色框架的边界,计算允许的最大偏移,让视图在边界处平滑停止而非突然中断。
  4. 命名规范修正:将原数据模型中的大写开头属性改为小写,符合Swift命名习惯。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 23:18:09