低仿扫描全能王的选择区域功能

2,413 阅读3分钟

扫描全能王 CS, Cam Scanner 很是强大,

本文简单仿一下他的选择区域功能。

端点可拖动

1, 识别点

本文例子比较简单,只有四个端点,用于拖拽

给定四个坐标,

先识别到开始点击的位置,距离哪个点近,

就认为要拖动那个点


    // 四个点,按方位划分
    enum SketchPointOption: Int{
        case leftTop = 0, rightTop = 1, leftBottom = 2
        case rightBottom = 3
    }



//	先识别到开始点击的位置,距离哪个点,在一定范围内
 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        super.touchesBegan(touches, with: event)
        
        guard let touch = touches.first else{
            return
        }
        
        
        let currentPoint = touch.location(in: self)
        
        // 判定选中的最大距离
        let maxDistance: CGFloat = 20
        let points = [defaultPoints.leftTop, defaultPoints.rightTop, defaultPoints.leftBottom,
                      defaultPoints.rightBottom]
        for pt in points{
            let distance = abs(pt.x - currentPoint.x) + abs(pt.y - currentPoint.y)
            if distance <= maxDistance, let pointIndex = points.firstIndex(of: pt){
                currentControlPointType = SketchPointOption(rawValue: pointIndex)
                break
            }
        }
    }
    


    // 如果上一步识别到了,就更新选择点的位置,并重新连线
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)
        if currentControlPointType != nil, let touch = touches.first{
            
            let current = touch.location(in: self)
            guard bounds.contains(current) else{
                return
            }
            // 就更新选择点的位置
            prepare(point: current)
            // 并重新连线
            reloadData()
        }
        
    }

2,更新 UI

本文中,图上四个点,没有用四个控件,

四个点用的是 CALayer, 事件处理采用上面的触摸检测,


    var lineLayer: CAShapeLayer = {
        let l = CAShapeLayer()
        l.lineWidth = 1
        l.fillColor = UIColor.clear.cgColor
        l.strokeColor = SketchColor.normal
        return l
    }()
    
    var pointsLayer: CAShapeLayer = {
        let l = CAShapeLayer()
        l.fillColor = UIColor.white.cgColor
        l.lineWidth = 2
        l.strokeColor = SketchColor.normal
        return l
    }()
    
    var linePath: UIBezierPath = {
        let l = UIBezierPath()
        l.lineWidth = 1
        return l
    }()
    
    var pointPath: UIBezierPath = {
        let l = UIBezierPath()
        l.lineWidth = 2
        return l
    }()


/**
     刷新数据
     */
    func reloadData(){
        linePath.removeAllPoints()
        pointPath.removeAllPoints()
        draw(sketch: defaultPoints)
        
       
        lineLayer.path = linePath.cgPath
        pointsLayer.path = pointPath.cgPath
    }
    
    
    /**
     绘制单个图形
     */
    func draw(sketch model: SketchModel){
        drawLine(with: model)
        drawPoints(with: model)
    }

    
    
    
    
    /**
      绘制四条边
     */
    func drawLine(with sketch: SketchModel){
        linePath.move(to: sketch.leftTop)
        linePath.addLine(to: sketch.rightTop)
        linePath.addLine(to: sketch.rightBottom)
        linePath.addLine(to: sketch.leftBottom)
        linePath.close()
        
    }
    
    
    
    
    /**
     绘制四个顶点
     */
    func drawPoints(with sketch: SketchModel){
        let radius: CGFloat = 8
        pointPath.move(to: sketch.leftTop.advance(radius))
        pointPath.addArc(withCenter: sketch.leftTop, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
        pointPath.move(to: sketch.rightTop.advance(radius))
        pointPath.addArc(withCenter: sketch.rightTop, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
        pointPath.move(to: sketch.rightBottom.advance(radius))
        pointPath.addArc(withCenter: sketch.rightBottom, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
        pointPath.move(to: sketch.leftBottom.advance(radius))
        pointPath.addArc(withCenter: sketch.leftBottom, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
        
        ///
     
    }

与上面结合, touchesBegan 识别到事件,

touchesMoved 就是不断的 reloadData, 不停重绘

最小区域检测

有四个点,选中点为当前点,

剩余点,为剩下的三个点。

移动的时候 touchesMoved,该点靠近其他三个点到一定距离,就取消


var currentControlPointType: SketchPointOption? = nil{
        didSet{
            if let type = currentControlPointType{
                var pts = [defaultPoints.leftTop, defaultPoints.rightTop, defaultPoints.leftBottom,
                           defaultPoints.rightBottom]
                pts.remove(at: type.rawValue)
                defaultPoints.restPoints = pts
            }
            else{
                defaultPoints.restPoints = []
            }
        }
    }



override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesMoved(touches, with: event)
        if currentControlPointType != nil, let touch = touches.first{
            
            
            let current = touch.location(in: self)
            guard bounds.contains(current) else{
                return
            }
            
            let points = defaultPoints.restPoints + [current]
            let ptCount = points.count
            for i in 0...(ptCount - 2){
                for j in (i + 1)...(ptCount - 1){
                    let lhs = points[i]
                    let rhs = points[j]
                    let distance = abs(lhs.x - rhs.x) + abs(lhs.y - rhs.y)
                    // 移动的时候 touchesMoved,该点靠近其他三个点到一定距离,就取消
                    // 这里的距离是 40 个 pt
                    if distance < 40{
                        ggTouch = true
                        break
                    }
                }
            }
            
            
            guard ggTouch == false else {
                
                return
            }
            
            // 更新选中点的坐标
            prepare(point: current)
            
         
            // 重新连线
            reloadData()
        }
        
    }

四个点比较好做,全能扫描王,8 个点,

端点拖,中点平移,

中点平移的时候,点的关系,稍复杂

常规的放大镜处理,

通过借助, CALayer 的 render(in:context) 方法,

再翻转一下坐标系,就好了

移动的时候,更新点,给 renderPoint ,就好了

具体的传值,见 GitHub repo

 class MagnifierView: UIView {

    
    public var renderView: UIView?
    
    public var renderPoint : CGPoint  = CGPoint.zero {
        didSet{
            self.layer.setNeedsDisplay()
        }
    }


    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        layer.borderWidth = 1
        layer.borderColor = UIColor.lightGray.cgColor
        isHidden = true
        layer.delegate = self
        // 保证和屏幕读取像素的比例一致
        layer.contentsScale = UIScreen.main.scale
    }
    

    
    override func draw(_ layer: CALayer, in ctx: CGContext) {
        super.draw(layer, in: ctx)
        guard let renderer = renderView else {
            return
        }
        // 提前位移半个长宽的坑
        ctx.translateBy(x: frame.size.width * 0.5, y: frame.size.height * 0.5)
        /// 缩放比例
        let scale : CGFloat = 3
        ctx.scaleBy(x: scale, y: scale)
        // 再次位移后就可以把触摸点移至self.center的位置
        ctx.translateBy(x: -renderPoint.x, y: -renderPoint.y)
        
        renderer.layer.render(in: ctx)
    }
    
}

凹四边形检测

通过图形学的公式

extension CGPoint{
    func gimpTransformPolygon(isConvex firstPt: CGPoint, two twicePt: CGPoint, three thirdPt: CGPoint) -> Bool{
        
        let x2 = firstPt.x, y2 = firstPt.y
        let x3 = twicePt.x, y3 = twicePt.y
        let x4 = thirdPt.x, y4 = thirdPt.y
     
        let z1 = ((x2 - x) * (y4 - y) - (x4 - x) * (y2 - y))
        let z2 = ((x4 - x) * (y3 - y) - (x3 - x) * (y4 - y))
        let z3 = ((x4 - x2) * (y3 - y2) - (x3 - x2) * (y4 - y2))
        let z4 = ((x3 - x2) * (y - y2) - (x - x2) * (y3 - y2))
     
        return (z1 * z2 > 0) && (z3 * z4 > 0)
    }
    
    
}

交叉重连

拖动,交叉了,重新连

出现了交叉,就更新点的连接方式

采用图形学的方法,

先判断一个点,在一条线的左边,还是右边


extension CGPoint{
    
    func pointSideLine(left lhs: CGPoint, right rhs: CGPoint) -> CGFloat{
        
        return (x - lhs.x) * (rhs.y - lhs.y) - (y - lhs.y) * (rhs.x - lhs.x)
        
    }
    
}

再判断四个点的顺时针顺序

struct SketchModel{
    var leftTop: CGPoint
    var rightTop: CGPoint
    var leftBottom: CGPoint
    var rightBottom: CGPoint
    
    var restPoints = [CGPoint]()
    
    
    var pts: [CGPoint]{
        [leftTop, rightTop, leftBottom, rightBottom]
    }
    

    mutating
    func sortPointClockwise() -> Bool{
         // 按左上,右上,右下,左下排序
        var result = [CGPoint](repeating: CGPoint.zero, count: 4)
        var minDistance: CGFloat = -1
        for p in pts{
            let distance = p.x * p.x + p.y * p.y
            if minDistance == -1 || distance < minDistance{
                result[0] = p
                minDistance = distance
            }
        }
        var leftPts = pts.filter { (pp) -> Bool in
            pp != result[0]
        }
        if leftPts[1].pointSideLine(left: result[0], right: leftPts[0]) * leftPts[2].pointSideLine(left: result[0], right: leftPts[0]) < 0{
            result[2] = leftPts[0]
        }
        else if leftPts[0].pointSideLine(left: result[0], right: leftPts[1]) * leftPts[2].pointSideLine(left: result[0], right: leftPts[1]) < 0{
            result[2] = leftPts[1]
        }
        else if leftPts[0].pointSideLine(left: result[0], right: leftPts[2]) * leftPts[1].pointSideLine(left: result[0], right: leftPts[2]) < 0{
            result[2] = leftPts[2]
        }
        leftPts = pts.filter { (pt) -> Bool in
            pt != result[0] && pt != result[2]
        }
        if leftPts[0].pointSideLine(left: result[0], right: result[2]) > 0{
            result[1] = leftPts[0]
            result[3] = leftPts[1]
        }
        else{
            result[1] = leftPts[1]
            result[3] = leftPts[0]
        }
        

        
        if result[0].gimpTransformPolygon(isConvex: result[1], two: result[3], three: result[2]){
        // 凸四边形,才有意义
        
            leftTop = result[0]
            rightTop = result[1]
            
            rightBottom = result[2]
            leftBottom = result[3]
            return true
        }
        else{
        
        // 无效图形,凹四边形,不用管
            return false
        }
        
    }
  
}


触摸结束的时候,判断下,就好

github repo