用CAShapeLayer来写一个简洁可点击的饼图

最近在开发一个记账软件,需要用到一个饼图来展示分类数据。作为一个骄傲的程序员怎么能不自己写一个,那么如何写一个漂亮的可点击的饼图呢?我首先想到的就是添加图形(CAShapeLayer+UIBezierPath),然后再让这些图形动起来呗(CAAnimation),那么如何响应点击事件呢?点、点击,就来一个touchesBegan方法,又怎么判断是不是点在我的饼图上呢?刚好UIBezierPath有个contains:point方法,能判断路径内是否包含一个点。万事俱备,开始打码。
首先新建一个PieView类

public class PieView: UIView {
}

我们先来想象一下我们的饼图应该长成什么样子:


想象中的样子

接下来为我们的PieView准备一些方法:

//重置属性,移除图层等
func reset() {

}
//没有数据时显示的动画
public func showEmptyAnimation() {

}
// 中间图层
func drawCenter() {

}
//根据(名称,值,颜色)画饼图
public func drawPurePie(_ dicts:[(name:String?, value:Float, color:UIColor)]){

}
//画扇形
fileprivate func drawSector(_ name:String?, _ startAg: CGFloat, _ endAg: CGFloat, _ color: UIColor, _ percent: Float) {

}

PieView用到属性

///记录上一个扇形的结束角度
var lastEndAg:CGFloat = 0.0
///扇形的宽度
var lineWidth:CGFloat = 40
///保存总计的值
var totalValue:Float = 0
///PieView的宽度
var width:CGFloat = 0
///PieView的高度
var height:CGFloat = 0
///饼图path的半径
var radius:CGFloat = 0
///饼图的中心
var arcCenter:CGPoint = .zero
///计算好的点击区域
lazy var tapPaths:[UIBezierPath] = [UIBezierPath]()
///点击后位移动画的路线
lazy var linePaths:[UIBezierPath] = [UIBezierPath]()
///饼图所有的扇形
lazy var sublayers:[CAShapeLayer] = [CAShapeLayer]()
///中间显示的文字
lazy var centerLabel:CATextLayer = CATextLayer()
///中间圆形的区域
var centerPath:UIBezierPath?

这里是PieView的所有动画

/// 画扇形的动画
lazy var strokeEnd: CABasicAnimation = {
    let animation = CABasicAnimation(keyPath: "strokeEnd")
    animation.fromValue = 0
    animation.toValue = 1
    animation.duration = 1
    animation.isRemovedOnCompletion = false
    animation.fillMode = kCAFillModeForwards
    
    return animation
}()
/// 加载动画
var loaddingAnimation: CAAnimationGroup {
    let rotation = CABasicAnimation(keyPath: "transform.rotation.z")
    rotation.fromValue = 0
    rotation.toValue = Double.pi * 2
    rotation.duration = 4
    rotation.beginTime = 0
    
    let strokeStart = CABasicAnimation(keyPath: "strokeStart")
    strokeStart.fromValue = 0
    strokeStart.toValue = 1
    strokeStart.duration = 2
    strokeStart.beginTime = 2
    
    let strokeEnd = CABasicAnimation(keyPath: "strokeEnd")
    strokeEnd.fromValue = 0
    strokeEnd.toValue = 1
    strokeEnd.duration = 2
    strokeEnd.beginTime = 0
    
    let group = CAAnimationGroup()
    group.duration = 4
    group.animations = [rotation, strokeStart, strokeEnd]
    group.fillMode = kCAFillModeBackwards
    group.repeatCount = .greatestFiniteMagnitude
    
    return group
}
///扇形位移动画
lazy var sectorPositionAnimation:CAKeyframeAnimation = {
    let position = CAKeyframeAnimation(keyPath: "position")
    position.duration = 0.1
    position.isRemovedOnCompletion = false
    position.fillMode = kCAFillModeForwards
    return position
}()
///扇形宽度动画
lazy var sectorWidthAnimation:CAAnimation = {
    let sector = CABasicAnimation(keyPath: "lineWidth")
    sector.fromValue = lineWidth
    sector.toValue = lineWidth * 1.2
    sector.duration = 0.1
    sector.isRemovedOnCompletion = false
    sector.fillMode = kCAFillModeForwards
    return sector
}()

准备了这么多,接下来要干正事了,先实现加载动画

///没有数据时显示的动画
public func showEmptyAnimation() {
    reset()

    let path = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
    let arc = CAShapeLayer()
    arc.frame = self.bounds
    arc.path = path.cgPath
    arc.strokeColor = UIColor.red.cgColor
    arc.fillColor = UIColor.clear.cgColor
    arc.lineWidth = 1
    
    arc.add(loaddingAnimation, forKey: "loaddingAnimation")
    layer.addSublayer(arc)
}

再画饼

///根据(名称,值,颜色)画饼图
public func drawPurePie(_ dicts:[(name:String?, value:Float, color:UIColor)]){
    reset()
    for dict in dicts {
        totalValue += dict.value
    }
    
    for (i,dict) in dicts.enumerated() {
        let color = dict.color
        let percent = dict.value / totalValue
        let angle = CGFloat(percent) * CGFloat.pi * 2
        let name = dict.name
        let sectorName = String(format: "%.f%%", percent * 100)
        drawLegend(name, color, i)
        drawSector(sectorName, lastEndAg, lastEndAg + angle, color, percent)
    }
    drawCenter()
}

实现饼图里面的扇形,中心部分,图例

/// 中间图层
func drawCenter() {
    centerPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
    let circle = CAShapeLayer()
    circle.path = centerPath?.cgPath
    circle.fillColor = UIColor.white.cgColor
    
    centerLabel.frame = CGRect(origin: .zero, size: CGSize(width: width * 0.5, height: 22))
    centerLabel.position = arcCenter
    centerLabel.contentsScale = UIScreen.main.scale
    centerLabel.fontSize = 20
    centerLabel.alignmentMode = kCAAlignmentCenter
    centerLabel.foregroundColor = UIColor.darkGray.cgColor
    centerLabel.string = "---"
    circle.addSublayer(centerLabel)
    layer.addSublayer(circle)
}
///画每一片扇形
fileprivate func drawSector(_ name:String?, _ startAg: CGFloat, _ endAg: CGFloat, _ color: UIColor, _ percent: Float) {
    lastEndAg = endAg
    
    ///点击后位移的路径
    let linePath = UIBezierPath()
    linePath.move(to: arcCenter)
    let midAg = (startAg + endAg) * 0.5
    linePath.addLine(to: CGPoint(x: arcCenter.x + cos(midAg) * 5, y: arcCenter.y +  sin(midAg) * 5))
    linePaths.append(linePath)
    ///可点击区域路径
    let tapPath = UIBezierPath()
    tapPath.move(to: arcCenter)
    tapPath.addArc(withCenter: arcCenter, radius: radius + lineWidth * 0.5, startAngle: startAg, endAngle: endAg, clockwise: true)
    tapPath.addLine(to: arcCenter)
    tapPaths.append(tapPath)
    ///添加CAShapeLayer
    let arc = CAShapeLayer()
    let arcPath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAg, endAngle: endAg, clockwise: true)
    arc.frame = bounds
    arc.name = name
    arc.path = arcPath.cgPath
    arc.strokeColor = color.cgColor ///UIColor.clear.cgColor
    arc.fillColor = UIColor.clear.cgColor
    arc.lineWidth = lineWidth
    arc.add(strokeEnd, forKey: "strokeEnd")
    sublayers.append(arc)
    layer.insertSublayer(arc, at: 0)
}

画好了图形,让我们来添加点击事件。判断点击时这里有一点需要注意的,因为是用的arc.lineWidth来实现扇形的,需要通过计算线外围和线内围的弧形来判断点是否在上面。在画线的时候算好内外圆的path,并保存在tapPaths中。

///点击事件
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    let point = touches.first?.location(in: self)
    for (i, subLayer) in sublayers.enumerated() {
        let tapPath = tapPaths[i]
        if point != nil && centerPath != nil && tapPath.contains(point!) && !centerPath!.contains(point!) {
            sectorWidthAnimation.fromValue = lineWidth
            sectorWidthAnimation.toValue = lineWidth * 1.2
            subLayer.add(sectorWidthAnimation, forKey: "sectorWidthAnimation")
            if sublayers.count > 1 {
                sectorPositionAnimation.path = linePaths[i].cgPath
                subLayer.add(sectorPositionAnimation, forKey: "sectorPositionAnimation")
            }
            centerLabel.string = subLayer.name
            print(subLayer)
        }else {
            subLayer.removeAllAnimations()
        }
    }
}

迫不及待的想试试的点这里代码传送门这里贴的是部分代码,让我们来看看最后实现的效果

点击前

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

推荐阅读更多精彩内容