计算遮罩边界的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) }
错误原因分析
原代码的计算逻辑存在两个核心问题:
- 当图片尺寸大于遮罩时,
maskSize.width - photoSize.width为负数,直接导致maxX/maxY为负数,限制逻辑完全反向 - 额外引入的
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
相关产品推荐
相关产品推荐

