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

计算遮罩边界的Offset Limit,解决拖拽图片越界问题

问题描述

各位开发者好,我正尝试设置偏移限制,确保在dragGesture拖拽过程中图片不会超出遮罩(mask)的轮廓。感谢Benzy Neez的帮助,我获取了用于图片的scaleOffset值,使原始偏移能适配图片尺寸变化,但此时偏移限制功能失效了。我肯定犯了某个低级错误,但找不到问题所在,以下是我的代码:

@MainActor
protocol PlatformView: View {
    var horizontalSizeClass: UserInterfaceSizeClass? { get }
    var verticalSizeClass: UserInterfaceSizeClass? { get }
    var isCompact: Bool { get }
    var isRegular: Bool { get }
}

extension PlatformView {
    var isCompact: Bool { horizontalSizeClass == .compact || verticalSizeClass == .compact }
    var isRegular: Bool { horizontalSizeClass == .regular && verticalSizeClass == .regular }
}

public enum Mask {
    case circle, square, rectangle(aspectRatio: AspectRatio)
    
    var shape: AnyShape {
        switch self {
        case .circle: return AnyShape(.circle)
        case .square, .rectangle : return AnyShape(.rect)
        }
    }
    
    func size(relativeTo size: CGSize) -> CGSize {
        let isLandscape = size.width > size.height
        let reference = isLandscape ? size.height : size
            .width

        switch self {
        case .circle, .square :
            return .init(width: reference, height: reference)
        
        case .rectangle(let aspectRatio):
            return .init(width: reference, height: reference * aspectRatio.ratio)
        }
    }

    public enum AspectRatio {
        case ar4_3, ar16_9
        var ratio: CGFloat {
            switch self {
            case .ar4_3: return 4/3
            case .ar16_9: return 16/9
            }
        }
    }
}

@Observable public class CroxioManagaer {
     
    var offset: CGSize = .zero
    var lastOffset: CGSize = .zero
    var photoSize: CGSize = .zero
    var maskSize: CGSize = .zero
    
    func currentOffset(translation: CGSize) -> CGSize  {
        let currentOffset: CGSize = .init(width: translation.width / photoSize.width, height: translation.height / photoSize.height)
        return lastOffset + currentOffset
       
    }
    
    func limitedOffset(translation: CGSize) {
        let ratioX = photoSize.width / maskSize.width
        let ratioY = photoSize.height / maskSize.height
        let maxX = ((maskSize.width - photoSize.width) / 2) / ratioX
        let maxY = ((maskSize.height - photoSize.height) / 2) / ratioY
        let currentOffset = currentOffset(translation: translation)
        let width = max(-maxX, min(maxX, currentOffset.width))
        let height = max(-maxY, min(maxY, currentOffset.height))
        offset = .init(width: width, height: height)
    }
    
    func saveOffset() {
        lastOffset = offset
    }
}

extension UIImage {
    private func aspectRatio(_ size: CGSize = .zero) -> CGFloat {
        return size == .zero ? self.size.width / self.size.height : size.width / size.height
    }
        
    func adaptPhotoSize(to size: CGSize) -> CGSize {
        let photoAspectRatio = aspectRatio()
        let maskAspectRatio = aspectRatio(size)
        
        if photoAspectRatio > maskAspectRatio {
            // 图片比遮罩宽:按高度缩放
            let width = size.height * photoAspectRatio
            return .init(width: width, height: size.height)
        } else {
            // 图片比遮罩高:按宽度缩放
            let height = size.width * photoAspectRatio
            return .init(width: size.width, height: height)
        }
    }
}

extension CGSize {
    static func + (lhs: CGSize, rhs: CGSize) -> CGSize {
        .init(width: lhs.width + rhs.width, height: lhs.height + rhs.height)
    }
}

public struct Croxio: PlatformView {
    var selectedPhoto: UIImage
    let prefersShape: Mask
    
    public init(selectedPhoto: UIImage, prefersShape: Mask = .circle) {
        self.selectedPhoto = selectedPhoto
        self.prefersShape = prefersShape
    }
    
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    @Environment(\.verticalSizeClass) var verticalSizeClass
    
    @State private var manager = CroxioManagaer()
    
    private var scaledOffset: CGSize {
        CGSize(
            width: manager.offset.width * manager.photoSize.width,
            height: manager.offset.height * manager.photoSize.height
        )
    }
    
    public var body: some View {
        ZStack {
            Image(uiImage: selectedPhoto)
                .resizable()
                .scaledToFill()
                .frame(manager.photoSize)
                .offset(scaledOffset)

            Color.primary.opacity(0.5)
                .mask {
                    Rectangle()
                        .overlay {
                            prefersShape.shape
                                .frame(manager.maskSize)
                                .blendMode(.destinationOut)
                        }
                }
        }
        .onGeometryChange(for: CGSize.self, of: { proxy in
            proxy.size
        }, action: { newValue in
            manager.maskSize = prefersShape.size(relativeTo: newValue * 0.7)
            manager.photoSize = selectedPhoto.adaptPhotoSize(to: manager.maskSize)
        })
        .ignoresSafeArea()
        .gesture(
            DragGesture()
                .onChanged({ value in
                    manager.limitedOffset(translation: value.translation)
                })
                .onEnded({ _ in manager.saveOffset() })
        )
    }
}

修复方案

问题出在CroxioManagaer的limitedOffset方法中,偏移限制的计算逻辑错误,导致限制范围反向或无效。以下是修正后的关键代码:

修正limitedOffset方法

func limitedOffset(translation: CGSize) {
    // 计算归一化后的最大允许偏移量:图片超出遮罩的一半,除以图片尺寸转为归一化值
    let maxX = (photoSize.width - maskSize.width) / 2 / photoSize.width
    let maxY = (photoSize.height - maskSize.height) / 2 / photoSize.height
    
    let currentOffset = self.currentOffset(translation: translation)
    // 将偏移量限制在合法范围内
    let clampedWidth = max(-maxX, min(maxX, currentOffset.width))
    let clampedHeight = max(-maxY, min(maxY, currentOffset.height))
    
    offset = CGSize(width: clampedWidth, height: clampedHeight)
}

错误原因分析

原代码的计算逻辑存在两个核心问题:

  1. 当图片尺寸大于遮罩时,maskSize.width - photoSize.width为负数,直接导致maxX/maxY为负数,限制逻辑完全反向
  2. 额外引入的ratioX/ratioY属于冗余计算,进一步放大了逻辑错误,实际上归一化偏移只需要基于图片自身尺寸计算即可

额外优化建议

可以在尺寸变化时重置偏移,避免图片位置异常:

.onGeometryChange(for: CGSize.self, of: { proxy in
    proxy.size
}, action: { newValue in
    manager.maskSize = prefersShape.size(relativeTo: newValue * 0.7)
    manager.photoSize = selectedPhoto.adaptPhotoSize(to: manager.maskSize)
    // 尺寸变化后重置偏移,避免图片位置错乱
    manager.offset = .zero
    manager.lastOffset = .zero
})

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 11:50:55