美文网首页程序员
用CAShapeLayer来写一个简洁可点击的饼图

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

作者: zhonglaoban | 来源:发表于2018-04-09 20:29 被阅读210次

最近在开发一个记账软件,需要用到一个饼图来展示分类数据。作为一个骄傲的程序员怎么能不自己写一个,那么如何写一个漂亮的可点击的饼图呢?我首先想到的就是添加图形(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()
        }
    }
}

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

点击前 点击后

相关文章

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

    最近在开发一个记账软件,需要用到一个饼图来展示分类数据。作为一个骄傲的程序员怎么能不自己写一个,那么如何写一个漂亮...

  • iOS 画个简单的饼图

    闲来无事,把之前仿照网友敲的饼图代码传上来,供以后查看,先看看效果图: 先创建一个CAShapeLayer子类,这...

  • 用UIPresentationController来写一个简洁的

    iOS App开发过程中,底部弹出框是一个非常常见的需求。实现这个需求的方式有很多,直接添加一个自定义的View让...

  • 用UIPresentationController来写一个简洁漂

    用UIPresentationController来写一个简洁漂亮的底部弹出控件 用UIPresentationC...

  • vue+elementUi+echarts 折线图组件

    echarts官网 效果 ? 此图并非折线图饼图联动 折线图饼图联动组件飞机票 ?饼图点击事件再饼图组件中 饼图组...

  • 飞行线地图使用饼图label

    点击地图或表格数据要使对应区域的弹框悬浮悬停,需要使用饼图的label,同时要给饼图定位 效果图 点击右边的表格,...

  • Android 最简单的饼状图(转)

    原文:android 最简单的饼状图 要做这么一个效果,我们应该分几步来写,1.先做一个静态的饼状图2.然后加上属...

  • 用小饼图代替点图里的点

    0.起因 最近在群里看到一个图,棒棒糖图的点用饼图代替了。 然后又看到一个文章里,用饼图代替了富集分析的点图,妙啊...

  • iOS Core Animation(四)- 子类

    只是简单了解几种子类 CAShapeLayer CAShapeLayer是一个通过矢量图形来绘制的图层子类,绘图可...

  • 饼图的可视化

    今天来看一下在ppt里如何美化饼图,达成上图所示的效果。 首先点击插入图表,然后选择饼图里的圆环图。 这时伴随着饼...

网友评论

    本文标题:用CAShapeLayer来写一个简洁可点击的饼图

    本文链接:https://www.haomeiwen.com/subject/bpcihftx.html