27
2017
09

一个好用的多边形控件

前言

image.png

要实现如图片中左侧的正六边形按钮,其中要有边框以及角的弧度。由于以前做过CALayer相关的功能,自然想起利用CALayer绘制path来实现该功能。

根据最大半径计算各顶点坐标

2460711-1410c2a76ad83148.png

先确定按钮的size得出最大r值,然后按照这个模式得出每个点相对于按钮的坐标,使用UIBezierPath绘制path得到最后的图样。按照这样的逻辑确实可以做出如UI展示的效果(先忽略圆角和边框),但是因此会引发一个问题:这种代码是一种死代码。本着代码的可扩展性原则,在此封装了一个多边形按钮

弧度计算顶点坐标

  • 首先进行一下逻辑分析,按照固定点坐标的做法会导致代码死板,因此换个思路进行设计: 使用角度的形式来设置对应点坐标。
  • 以按钮的中心点centerPoint作为原点,按钮所在坐标系的x轴上点(r, 0)点作为绘制的起始点,依据顺时针方向依次计算每个点的坐标
  • 根据规律可知对应弧度所表示的点的坐标的x值等于centerPoint.x +
    r * cos(弧度)
    ,而y值等于centerPoint.y + r * sin(弧度) 该坐标既是多边形对应顶点的坐标

获取单个点的坐标

    private func vertexCoordinates(radius: CGFloat, angle: Double, offset: Double = 0) ->CGPoint {
        let centerPoint = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
        let X = centerPoint.x + radius * (angle + offset).cosValue
        let Y = centerPoint.y + radius * (angle + offset).sinValue
        return CGPoint(x: X, y: Y)
    }

根据多边形的边数、最大半径以及偏移弧度获得多边形的每个顶点的坐标

    private func regularPolygonCoordinates(sides: Int, radius: CGFloat, offset: Double = 0) ->[CGPoint] {
        assert(sides >= 3, "多边形最少为3边")
        assert(radius > 0, "多边形半径必须大于0")
        var coordinates = [CGPoint]()
        for i in 0..<sides {
            let corner = Double(360) / Double(sides)
            let radian = corner / Double(180) * Double.pi
            let radianOfPoint = Double(i) * radian
            let point = vertexCoordinates(radius: radius, angle: radianOfPoint, offset: offset)
            coordinates.append(point)
        }
        return coordinates
    }

再进一步根据获得的各个顶点的坐标数组可以用UIBezierPath绘制图案

            let points = regularPolygonCoordinates(sides: style.sides, radius: r, offset: style.offset)

            for (index, point) in points.enumerated() {
                if index == 0 {
                    path.move(to: point)
                }else {
                    path.addLine(to: point)
                }
            }
            path.close()

到现在只是得到了完整的path还需要设置到layer上才能变相的按该路径裁剪按钮

    private func hexagon() {
        var Max_R: CGFloat
        if style.Max_Radius == 0 {
            let W = bounds.width
            let H = bounds.height
            assert(W > 0 && H > 0, "此时宽或者高没有值")
            Max_R = H > W ? W / 2 : H / 2
        }else {
            Max_R = style.Max_Radius
        }

        assert(Max_R > style.borderWidth, "多边形最大半径不能小于边界宽度")

        for (index, layer) in self.layer.sublayers!.enumerated() {
            if index != 0 {
                layer.removeFromSuperlayer()
            }
        }

        let topLayer = CAShapeLayer()
        topLayer.path = drawPath(radius: Max_R)
        topLayer.strokeColor = style.borderColor.cgColor
        topLayer.fillColor = UIColor.clear.cgColor
        topLayer.lineWidth = style.borderWidth
        let bottomLayer = CAShapeLayer()
        bottomLayer.path = drawPath(radius: Max_R)

        self.layer.mask = bottomLayer
        self.layer.insertSublayer(topLayer, above: bottomLayer)
    }

然后在layoutSubviews()方法中调用该绘制方法即可获得对应图形的按钮

override func layoutSubviews() {
        super.layoutSubviews()

        hexagon()
    }

圆角的设置

我这里的圆角采用的不是平时按钮的cornerRadius,而是采用的贝塞尔曲线的形式设置圆角,即根据两个定点以及一个控制点来绘制一条有弧度的曲线,原因是多边形如果设置cornerRadius会导致曲率很大最后裁剪出的弧度十分难看(不认同的话欢迎指正)

A226DCFDDF69510054DD2C8A47A2CA48.jpg

如果把正多边形中心点顶点邻近顶点组成的等边三角形视为一个子模块,选取贝塞尔曲线的固定两点其一为D点(另一点以AC边对称)与控制点C,则只需要计算出D点对应的坐标即可(C点坐标已知,r最大半径已知,CD长度自定义,多边形边数自定义)

  • 首先需要计算出∠DAC角的弧度
  • 然后计算出AD边的大小
  • 根据普通正多边形各顶点坐标的规律计算出ADR的多边形相对于原多边形偏移∠DAC角度的各顶点坐标
private func regularPolygonCoordinatesWithRoundedCorner(sides: Int, radius: CGFloat, offset: Double = 0) ->[CGPoint] {
        assert(sides >= 3, "多边形最少为3边")
        assert(radius > 0, "多边形半径必须大于0")
        let CAB = Double(360) / Double(sides) / Double(180) * Double.pi
        let EC = Double(radius * (CAB / 2).sinValue)
        let AE = Double(radius * (CAB / 2).cosValue)
        let ED = EC - style.filletDegree
        let EAD = atan(ED / AE)
        let DAC = CAB / 2 - EAD
        let newRadius = sqrt(pow(AE, 2) + pow(ED, 2))
        var coordinates = [CGPoint]()
        for i in 0..<sides {
            let direction = Double(i) * Double(360) / Double(sides) / Double(180) * Double.pi
            let point = vertexCoordinates(radius: radius, angle: direction, offset: offset)
            let leftAngle = direction - DAC
            let leftPoint = vertexCoordinates(radius: CGFloat(newRadius), angle: leftAngle, offset: offset)
            let rightAngle = direction + DAC
            let rightPoint = vertexCoordinates(radius: CGFloat(newRadius), angle: rightAngle, offset: offset)
            coordinates.append(leftPoint)
            coordinates.append(point)
            coordinates.append(rightPoint)
        }
        return coordinates
    }

通过上面的代码逻辑可以计算出有圆角的多边形的各关键点坐标,然后就只需要把这些关键点根据规律连接到一起。

let points = regularPolygonCoordinatesWithRoundedCorner(sides: style.sides, radius: r, offset: style.offset)
            var temPoint: CGPoint!
            for (index, point) in points.enumerated() {
                if index == 0 {
                    path.move(to: point)
                }else {
                    let remainder = index % 3
                    switch remainder {
                    case 0: 
                        path.addLine(to: point)
                    case 1: 
                        temPoint = point
                    case 2: 
                        path.addQuadCurve(to: point, controlPoint: temPoint)
                    default:
                        break
                    }
                }
            }
            path.close()

封装控制属性


class SYPolygonStyle {
    /// 是否可以圆角
    var roundedCornersEnable: Bool = false
    /// 圆角程度 - 值越大,角越平滑
    var filletDegree: Double = 5.0

    /// 边界宽度(边线的宽度)
    var borderWidth: CGFloat = 0.0
    /// 边界颜色
    var borderColor: UIColor = UIColor.gray

    /// 多边形最大半径 -- 如果不设置该值, 默认是按钮中可显示的最大多边形的半径
    var Max_Radius: CGFloat = 0.0

    /// 整个路径以按钮的中心点为中心按顺时针方向偏移的弧度(需要传入一个带π的弧度) -- 默认六边形顶点为水平方向, 如果设置该值为π/2则顶点为竖直方向
    var offset: Double = 0
    /// 多边形的边数 - 默认是正六边形
    var sides: Int = 6
}

当然,有了控制属性的类没有使用场景怎么行,只需要一个新的构造方法即可

init(frame: CGRect = CGRect.zero, style: SYPolygonStyle = SYPolygonStyle()) {
        self.style = style
        super.init(frame: frame)
    }

使用方法类似于下例:

        let style = SYPolygonStyle()
        style.filletDegree = 10
        style.borderWidth = 10
        style.borderColor = .orange
        style.roundedCornersEnable = true
        style.offset = Double.pi / 4
        style.sides = 5

        let liubianxing = SYPolygonButton(frame: CGRect.zero, style: style)
        liubianxing.setImage(UIImage.init(named: "zhbd_qq_icon"), for: .normal)
        liubianxing.setImage(UIImage.init(named: "zhbd_weibo_icon"), for: .selected)
        liubianxing.addTarget(self, action: #selector(btnClickAction(sender:)), for: .touchUpInside)
        view.addSubview(liubianxing)
        liubianxing.snp.makeConstraints { (make) in
            make.top.left.equalTo(100)
            make.width.height.equalTo(100)
        }

Demo


谷胜亚初次编辑于2017年9月25日

上一篇:使用cJSON解析JSON字符串 下一篇:策略模式