iOS开发CALayer绘制内容超出UIView边界的裁剪问题求解
问题描述
开发iOS手绘组件时出现笔触超出绘制区域显示的异常:图示异常表现
在继承自UIView的Page组件边界内落笔后,手指滑动到Page边界外侧时,超出边界的笔触部分依然会显示,不符合预期。
预期实现效果:支持用户从Page内部落笔后滑动到边界外持续绘制连续笔触,但超出Page边界的笔触部分不显示。
补充说明:当前实现中UIBezierCurve的坐标原点(0,0)为Page白色绘制区域的左上角,而非整个上层视图的左上角,因此从Page内部延伸到外侧的贝塞尔曲线坐标会出现负值。
解决方案
问题根源是UIView默认不会裁剪超出自身bounds范围的子视图、子图层内容,现有代码直接把笔触CAShapeLayer添加到Page的根layer上,超出边界的路径部分会正常渲染显示。不需要修改现有手势处理、贝塞尔曲线生成逻辑,通过边界裁剪即可实现需求,有两种可选实现方式:
方案1:全局开启视图边界裁剪(实现最简单,优先推荐)
直接在Page的两个初始化方法中开启裁剪属性,所有添加到Page上的子视图、子图层只要超出Page边界都会被自动裁剪,改动量极小:
override init(frame: CGRect) { super.init(frame: .zero) clipsToBounds = true layer.masksToBounds = true } required init?(coder: NSCoder) { super.init(coder: coder) clipsToBounds = true layer.masksToBounds = true }
属性说明:
clipsToBounds:UIView内置属性,设为true时自动裁剪所有超出视图bounds的子视图内容layer.masksToBounds:CALayer内置属性,设为true时自动裁剪所有超出layer边界的子层内容,正好覆盖直接添加到view.layer上的所有笔触CAShapeLayer
方案2:单笔触层独立裁剪(适配需要展示部分超出边界元素的场景)
如果后续需要在Page上展示超出边界的元素(比如笔触选中控制点、页面装饰元素),不适合全局裁剪,可以给每个笔触的CAShapeLayer单独配置裁剪遮罩,只保留Page边界内的笔触部分。
在每次创建updatedCALayer(CAShapeLayer实例)的位置添加如下代码即可:
// 初始化updatedCALayer的原有代码保留 let updatedCALayer = CAShapeLayer() updatedCALayer.path = currentBezier.cgPath updatedCALayer.lineWidth = selectedPen.width updatedCALayer.opacity = selectedPen.strokeOpacity updatedCALayer.strokeColor = selectedPen.getStroke().cgColor updatedCALayer.fillColor = selectedPen.getFill() // 新增:给当前笔触层添加和Page边界一致的遮罩 let clipMask = CAShapeLayer() clipMask.path = UIBezierPath(rect: bounds).cgPath updatedCALayer.mask = clipMask
实现注意事项
- 开启裁剪后不需要修改现有手势响应逻辑,手指滑到Page外侧时依然可以正常获取触摸坐标、生成连续的贝塞尔曲线,仅超出边界的笔触部分不会被渲染,完全匹配需求
- 如果Page尺寸会动态变化(比如横竖屏切换、调整纸张规格),使用方案1不需要额外适配;使用方案2需要重写
layoutSubviews方法,更新所有已存储笔触layer的遮罩路径,避免裁剪区域和实际Page尺寸错位
问题涉及的核心代码
import UIKit class Page: UIView { var bezierMemory = [BezierRecord]() var currentBezier: UIBezierPath = UIBezierPath() var firstPoint: CGPoint = CGPoint() var previousPoint: CGPoint = CGPoint() var morePreviousPoint: CGPoint = CGPoint() var previousCALayer: CALayer = CALayer() var pointCounter = 0 var selectedPen: Pen = Pen(width: 3.0, strokeOpacity: 1, strokeColor: .red, fillColor: .init(gray: 0, alpha: 0.5), isPencil: true, connectsToStart: true, fillPencil: true) enum StandardPageSizes { case A4, LEGAL, LETTER } var firstCALayer = true var pointsTotal = 0 override init(frame: CGRect) { super.init(frame: .zero) } required init?(coder: NSCoder) { super.init(coder: coder) } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesBegan(touches, with: event) guard let touch = touches.first else { return } let point = touch.location(in: self) firstPoint = point pointCounter = 1 currentBezier = UIBezierPath() currentBezier.lineWidth = selectedPen.width selectedPen.getStroke().setStroke() currentBezier.move(to: point) previousPoint = point } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesMoved(touches, with: event) guard let touch = touches.first else { return } let point = touch.location(in: self) pointCounter += 1 if (pointCounter == 3) { let midpoint = CGPoint(x: (morePreviousPoint.x + point.x)/2.0, y: (morePreviousPoint.y + point.y)/2.0) currentBezier.addQuadCurve(to: midpoint, controlPoint: morePreviousPoint) let updatedCALayer = CAShapeLayer() updatedCALayer.path = currentBezier.cgPath updatedCALayer.lineWidth = selectedPen.width updatedCALayer.opacity = selectedPen.strokeOpacity updatedCALayer.strokeColor = selectedPen.getStroke().cgColor updatedCALayer.fillColor = selectedPen.getFill() if (firstCALayer) { layer.addSublayer(updatedCALayer) firstCALayer = false } else { layer.replaceSublayer(previousCALayer, with: updatedCALayer) } previousCALayer = updatedCALayer pointCounter = 1 } morePreviousPoint = previousPoint previousPoint = point } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesEnded(touches, with: event) guard let touch = touches.first else { return } let point = touch.location(in: self) if (pointCounter != 3) { if (selectedPen.connectsToStart) { currentBezier.addQuadCurve(to: firstPoint, controlPoint: previousPoint) } else { currentBezier.addQuadCurve(to: point, controlPoint: previousPoint) } let updatedCALayer = CAShapeLayer() updatedCALayer.path = currentBezier.cgPath updatedCALayer.lineWidth = selectedPen.width updatedCALayer.opacity = selectedPen.strokeOpacity updatedCALayer.strokeColor = selectedPen.getStroke().cgColor updatedCALayer.fillColor = selectedPen.getFill() if (firstCALayer) { layer.addSublayer(updatedCALayer) firstCALayer = false } else { layer.replaceSublayer(previousCALayer, with: updatedCALayer) } } firstCALayer = true let bezierRecord = BezierRecord(bezier: currentBezier, strokeColor: selectedPen.getStroke(), fillColor: selectedPen.getFill(), strokeWidth: selectedPen.width) bezierMemory.append(bezierRecord) } private func normPoint(point: CGPoint) -> CGPoint { return CGPoint(x: point.x/frame.width, y: point.y/frame.height) } public class BezierRecord { var bezier: UIBezierPath var strokeColor: UIColor var strokeWidth: CGFloat var fillColor: CGColor init(bezier: UIBezierPath, strokeColor: UIColor, fillColor: CGColor, strokeWidth: CGFloat) { self.bezier = bezier self.strokeColor = strokeColor self.strokeWidth = strokeWidth self.fillColor = fillColor } } }
内容的提问来源于stack exchange,提问作者KindestPath0b
相关产品推荐
相关产品推荐

