最近在开发一个记账软件,需要用到一个饼图来展示分类数据。作为一个骄傲的程序员怎么能不自己写一个,那么如何写一个漂亮的可点击的饼图呢?我首先想到的就是添加图形(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()
}
}
}
迫不及待的想试试的点这里代码传送门这里贴的是部分代码,让我们来看看最后实现的效果