用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, 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()
        }
    }
}

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

点击前

点击后

你可能感兴趣的:(用CAShapeLayer来写一个简洁可点击的饼图)