也许对于完美的形状来说,有一个更简单的解决方案:假设您知道要查找的形状,您可以在用户的UIBezierPath范围内创建该形状的路径,然后针对该路径的描边版本执行命中测试。
例如,下面是一个检测路径是否类似于椭圆形的Swift 5方法。我还添加了一个从另一篇帖子中改编的方法,用于提取所有UIBezierPath的元素,除了子路径的闭合点:
extension UIBezierPath {
func resemblesOval() -> Bool {
let pathPoints = self.getPathElements().map({$0.point})
let perfectOvalPath = UIBezierPath(ovalIn: self.bounds)
let hitWidth = (self.bounds.width + self.bounds.height)/2 * 0.25
let hitPath = perfectOvalPath.cgPath.copy(strokingWithWidth: hitWidth,
lineCap: .round,
lineJoin: .miter,
miterLimit: 0)
var validPathPoints = [CGPoint]()
for point in pathPoints {
guard point != nil else { continue }
if hitPath.contains(point!) {
validPathPoints.append(point!)
}
}
if 10 * validPathPoints.count >= 9 * pathPoints.count {
return true
}
return false
}
enum PathElementType {
case move
case addLine
case addQuadCurve
case addCurve
}
func getPathElements() -> [(type: PathElementType?, point: CGPoint?, controlPoint: CGPoint?, controlPoint1: CGPoint?, controlPoint2: CGPoint?)] {
let initialPath = UIBezierPath(cgPath: self.cgPath)
var bezierPoints = NSMutableArray()
initialPath.cgPath.apply(info: &bezierPoints, function: { info, element in
guard let resultingPoints = info?.assumingMemoryBound(to: NSMutableArray.self) else {
return
}
let points = element.pointee.points
let type = element.pointee.type
switch type {
case .moveToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add(NSString("move"))
case .addLineToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add(NSString("addLine"))
case .addQuadCurveToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])
resultingPoints.pointee.add(NSString("addQuadCurve"))
case .addCurveToPoint:
resultingPoints.pointee.add([NSNumber(value: Float(points[0].x)), NSNumber(value: Float(points[0].y))])
resultingPoints.pointee.add([NSNumber(value: Float(points[1].x)), NSNumber(value: Float(points[1].y))])
resultingPoints.pointee.add([NSNumber(value: Float(points[2].x)), NSNumber(value: Float(points[2].y))])
resultingPoints.pointee.add(NSString("addCurve"))
case .closeSubpath:
break
@unknown default:
break
}
})
let elementsTypes : [String] = bezierPoints.compactMap { $0 as? String }
let elementsCGFloats : [[CGFloat]] = bezierPoints.compactMap { $0 as? [CGFloat] }
var elementsCGPoints : [CGPoint] = elementsCGFloats.map { CGPoint(x: $0[0], y: $0[1]) }
var returnValue : [(type: PathElementType?, point: CGPoint?, controlPoint: CGPoint?, controlPoint1: CGPoint?, controlPoint2: CGPoint?)] = []
for i in 0..<elementsTypes.count {
switch elementsTypes[i] {
case "move":
returnValue.append((type: .move, point: elementsCGPoints.removeFirst(), controlPoint: nil, controlPoint1: nil, controlPoint2: nil))
case "addLine":
returnValue.append((type: .addLine, point: elementsCGPoints.removeFirst(), controlPoint: nil, controlPoint1: nil, controlPoint2: nil))
case "addQuadCurve":
let controlPoint = elementsCGPoints.removeFirst()
returnValue.append((type: .addQuadCurve, point: elementsCGPoints.removeFirst(), controlPoint: controlPoint, controlPoint1: nil, controlPoint2: nil))
case "addCurve":
let controlPoint1 = elementsCGPoints.removeFirst()
let controlPoint2 = elementsCGPoints.removeFirst()
returnValue.append((type: .addCurve, point: elementsCGPoints.removeFirst(), controlPoint: nil, controlPoint1: controlPoint1, controlPoint2: controlPoint2))
default:
returnValue.append((type: nil, point: nil, controlPoint: nil, controlPoint1: nil, controlPoint2: nil))
}
}
return returnValue
}
}