正多边形按钮--可圆角可旋转可边框

前言

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)
        }

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 前言 多边形偏移 (polygon offset) 算法可能我们印象不深,不过用过 autoCAD 的同学应该有印...
    zyl06阅读 11,021评论 19 14
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,451评论 25 707
  • 一:canvas简介 1.1什么是canvas? ①:canvas是HTML5提供的一种新标签 ②:HTML5 ...
    GreenHand1阅读 4,661评论 2 32
  • 什么也不想写,也不知道写些什么,那么,今天,我就写一写自己的不想写吧。 从书写开始的那一天,我就每每拖到晚上开始,...
    赵婉宁阅读 452评论 0 0
  • 缘起 2016年初,早上醒来偶尔会觉得最里面的牙有些不大舒服,但不舒服的频率大概也就两周一次,持续时间大概是早上上...
    出走De娜拉阅读 5,846评论 0 1