核心动画中的简单饼图
Simple Pie Chart in Core Animation
我正在尝试使用 Core Animation 在我的应用程序中包含一个简单的饼图。
网上找了一篇文章复制调整,貌似和我需要的差不多。
https://github.com/tomnoda/piechart_ios
该代码引用了 Nib 文件(我不是很了解),但我可以通过编程来代替吗?我认为这是需要更改的代码行,也许我还需要添加一些其他编码:-
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let view: UIView = Bundle.main.loadNibNamed("PieChartView", owner: self, options: nil)!.first as! UIView
addSubview(view)
let 行引用了 Nib 文件,但我怎样才能让它引用我的视图控制器呢?
这显然会导致一系列未解决的标识符错误,因为这 2 个文件没有按应有的方式链接。在 View Controller 上,我有以下内容以及许多其他渠道:-
@IBOutlet weak var pieChartView: PieChartView!
由于我是 Xcode 的新手,希望这个问题有一个简单的解决方法。
"I'm trying to include a simple pie chart in my app using Core Animation"
首先,从该语句中删除单词 simple。听起来不像是个混蛋,但如果您是初学者,甚至不了解 nib (xib) 中布局的元素与通过代码创建元素,那么您将有很长的路要走。
虽然您链接到 "works," 的示例有很多限制,并且采用了一些相当奇怪的方法来完成任务。例如:
- 限制为 5 个或更少的段
- 段值的总和必须等于 1.0
- 它几乎没有错误检查的方式
也就是说,它可能是您开始学习的好地方。
这是相同的代码,修改为不需要 xib 文件。可以这样使用:
class ViewController: UIViewController {
@IBOutlet var pieChartView: MyPieChartView!
override func viewDidLoad() {
super.viewDidLoad()
pieChartView.slices = [
Slice(percent: 0.4, color: UIColor.red),
Slice(percent: 0.3, color: UIColor.blue),
Slice(percent: 0.2, color: UIColor.purple),
Slice(percent: 0.1, color: UIColor.green)
]
}
override func viewDidAppear(_ animated: Bool) {
pieChartView.animateChart()
}
}
这是MyPieChartView.swift
...
原始 PieChartView.swift
文件的第一个更改位于顶部,介于:
// MARK: Changes start here
// MARK: Changes end here
其他更改以允许 "anti-clockwise" ... 查找新 Bool var 的实例 drawClockwise
import UIKit
class MyPieChartView: UIView {
static let ANIMATION_DURATION: CGFloat = 1.4
// MARK: Changes start here
var canvasView: UIView!
var label1: UILabel!
var label2: UILabel!
var label3: UILabel!
var label4: UILabel!
var label5: UILabel!
var label1XConst: NSLayoutConstraint!
var label2XConst: NSLayoutConstraint!
var label3XConst: NSLayoutConstraint!
var label4XConst: NSLayoutConstraint!
var label5XConst: NSLayoutConstraint!
var label1YConst: NSLayoutConstraint!
var label2YConst: NSLayoutConstraint!
var label3YConst: NSLayoutConstraint!
var label4YConst: NSLayoutConstraint!
var label5YConst: NSLayoutConstraint!
var drawClockwise: Bool = true
var slices: [Slice]?
var sliceIndex: Int = 0
var currentPercent: CGFloat = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
if canvasView == nil {
let container = UIView()
addSubview(container)
canvasView = UIView()
container.addSubview(canvasView)
canvasView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
canvasView.topAnchor.constraint(equalTo: container.topAnchor),
canvasView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
canvasView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
canvasView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
canvasView.backgroundColor = .yellow
label1 = UILabel()
label2 = UILabel()
label3 = UILabel()
label4 = UILabel()
label5 = UILabel()
[label1, label2, label3, label4, label5].forEach {
guard let v = [=12=] else { fatalError("Bad Setup!") }
v.translatesAutoresizingMaskIntoConstraints = false
v.textColor = .white
v.textAlignment = .center
addSubview(v)
}
label1XConst = label1.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label1YConst = label1.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label2XConst = label2.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label2YConst = label2.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label3XConst = label3.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label3YConst = label3.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label4XConst = label4.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label4YConst = label4.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label5XConst = label5.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label5YConst = label5.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
[label1XConst, label2XConst, label3XConst, label4XConst, label5XConst,
label1YConst, label2YConst, label3YConst, label4YConst, label5YConst].forEach {
[=12=]?.isActive = true
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
subviews[0].frame = bounds
}
// don't do this
//override func draw(_ rect: CGRect) {
// subviews[0].frame = bounds
//}
// MARK: Changes end here
/// Get an animation duration for the passed slice.
/// If slice share is 40%, for example, it returns 40% of total animation duration.
///
/// - Parameter slice: Slice struct
/// - Returns: Animation duration
func getDuration(_ slice: Slice) -> CFTimeInterval {
return CFTimeInterval(slice.percent / 1.0 * PieChartView.ANIMATION_DURATION)
}
/// Convert slice percent to radian.
///
/// - Parameter percent: Slice percent (0.0 - 1.0).
/// - Returns: Radian
func percentToRadian(_ percent: CGFloat) -> CGFloat {
//Because angle starts wtih X positive axis, add 270 degrees to rotate it to Y positive axis.
var angle = 270 + percent * 360
if angle >= 360 {
angle -= 360
}
return angle * CGFloat.pi / 180.0
}
/// Add a slice CAShapeLayer to the canvas.
///
/// - Parameter slice: Slice to be drawn.
func addSlice(_ slice: Slice) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = getDuration(slice)
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.delegate = self
let canvasWidth = canvasView.frame.width
let toPercent = currentPercent + (drawClockwise ? slice.percent : -slice.percent)
let path = UIBezierPath(arcCenter: canvasView.center,
radius: canvasWidth * 3 / 8,
startAngle: percentToRadian(currentPercent),
endAngle: percentToRadian(toPercent),
clockwise: drawClockwise)
let sliceLayer = CAShapeLayer()
sliceLayer.path = path.cgPath
sliceLayer.fillColor = nil
sliceLayer.strokeColor = slice.color.cgColor
sliceLayer.lineWidth = canvasWidth * 2 / 8
sliceLayer.strokeEnd = 1
sliceLayer.add(animation, forKey: animation.keyPath)
canvasView.layer.addSublayer(sliceLayer)
}
/// Get label's center position based on from and to percentages.
/// This is always relative to canvasView's center.
///
/// - Parameters:
/// - fromPercent: End of previous slice.
/// - toPercent: End of current slice.
/// - Returns: Center point for label.
func getLabelCenter(_ fromPercent: CGFloat, _ toPercent: CGFloat) -> CGPoint {
let radius = canvasView.frame.width * 3 / 8
let labelAngle = percentToRadian((toPercent - fromPercent) / 2 + fromPercent)
let path = UIBezierPath(arcCenter: canvasView.center,
radius: radius,
startAngle: labelAngle,
endAngle: labelAngle,
clockwise: drawClockwise)
path.close()
return path.currentPoint
}
/// Re-position and draw label such as "43%".
///
/// - Parameter slice: Slice whose label is drawn.
func addLabel(_ slice: Slice) {
let center = canvasView.center
let labelCenter = getLabelCenter(currentPercent, currentPercent + (drawClockwise ? slice.percent : -slice.percent))
let xConst = [label1XConst, label2XConst, label3XConst, label4XConst, label5XConst][sliceIndex]
let yConst = [label1YConst, label2YConst, label3YConst, label4YConst, label5YConst][sliceIndex]
xConst?.constant = labelCenter.x - center.x
yConst?.constant = labelCenter.y - center.y
let label = [label1, label2, label3, label4, label5][sliceIndex]
label?.isHidden = true
label?.text = String(format: "%d%%", Int(slice.percent * 100))
}
/// Call this to start pie chart animation.
func animateChart() {
sliceIndex = 0
currentPercent = 0.0
canvasView.layer.sublayers = nil
if slices != nil && slices!.count > 0 {
let firstSlice = slices![0]
addLabel(firstSlice)
addSlice(firstSlice)
}
}
}
extension MyPieChartView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if flag {
currentPercent += (drawClockwise ? slices![sliceIndex].percent : -slices![sliceIndex].percent)
sliceIndex += 1
if sliceIndex < slices!.count {
let nextSlice = slices![sliceIndex]
addLabel(nextSlice)
addSlice(nextSlice)
} else {
//After animation is done, display all labels. Can be animated.
for label in [label1, label2, label3, label4, label5] {
label?.isHidden = false
}
}
}
}
}
示例:
class ViewController: UIViewController {
@IBOutlet var pieChartView: MyPieChartView!
@IBOutlet var antiPieChartView: MyPieChartView!
override func viewDidLoad() {
super.viewDidLoad()
pieChartView.slices = [
Slice(percent: 0.4, color: UIColor.red),
Slice(percent: 0.3, color: UIColor.blue),
Slice(percent: 0.2, color: UIColor.purple),
Slice(percent: 0.1, color: UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0))
]
antiPieChartView.slices = [
Slice(percent: 0.4, color: UIColor.red),
Slice(percent: 0.3, color: UIColor.blue),
Slice(percent: 0.2, color: UIColor.purple),
Slice(percent: 0.1, color: UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0))
]
// draw this pie anti-clockwise
antiPieChartView.drawClockwise = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
pieChartView.animateChart()
antiPieChartView.animateChart()
}
}
我正在尝试使用 Core Animation 在我的应用程序中包含一个简单的饼图。 网上找了一篇文章复制调整,貌似和我需要的差不多。
https://github.com/tomnoda/piechart_ios
该代码引用了 Nib 文件(我不是很了解),但我可以通过编程来代替吗?我认为这是需要更改的代码行,也许我还需要添加一些其他编码:-
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let view: UIView = Bundle.main.loadNibNamed("PieChartView", owner: self, options: nil)!.first as! UIView
addSubview(view)
let 行引用了 Nib 文件,但我怎样才能让它引用我的视图控制器呢? 这显然会导致一系列未解决的标识符错误,因为这 2 个文件没有按应有的方式链接。在 View Controller 上,我有以下内容以及许多其他渠道:-
@IBOutlet weak var pieChartView: PieChartView!
由于我是 Xcode 的新手,希望这个问题有一个简单的解决方法。
"I'm trying to include a simple pie chart in my app using Core Animation"
首先,从该语句中删除单词 simple。听起来不像是个混蛋,但如果您是初学者,甚至不了解 nib (xib) 中布局的元素与通过代码创建元素,那么您将有很长的路要走。
虽然您链接到 "works," 的示例有很多限制,并且采用了一些相当奇怪的方法来完成任务。例如:
- 限制为 5 个或更少的段
- 段值的总和必须等于 1.0
- 它几乎没有错误检查的方式
也就是说,它可能是您开始学习的好地方。
这是相同的代码,修改为不需要 xib 文件。可以这样使用:
class ViewController: UIViewController {
@IBOutlet var pieChartView: MyPieChartView!
override func viewDidLoad() {
super.viewDidLoad()
pieChartView.slices = [
Slice(percent: 0.4, color: UIColor.red),
Slice(percent: 0.3, color: UIColor.blue),
Slice(percent: 0.2, color: UIColor.purple),
Slice(percent: 0.1, color: UIColor.green)
]
}
override func viewDidAppear(_ animated: Bool) {
pieChartView.animateChart()
}
}
这是MyPieChartView.swift
...
原始 PieChartView.swift
文件的第一个更改位于顶部,介于:
// MARK: Changes start here
// MARK: Changes end here
其他更改以允许 "anti-clockwise" ... 查找新 Bool var 的实例 drawClockwise
import UIKit
class MyPieChartView: UIView {
static let ANIMATION_DURATION: CGFloat = 1.4
// MARK: Changes start here
var canvasView: UIView!
var label1: UILabel!
var label2: UILabel!
var label3: UILabel!
var label4: UILabel!
var label5: UILabel!
var label1XConst: NSLayoutConstraint!
var label2XConst: NSLayoutConstraint!
var label3XConst: NSLayoutConstraint!
var label4XConst: NSLayoutConstraint!
var label5XConst: NSLayoutConstraint!
var label1YConst: NSLayoutConstraint!
var label2YConst: NSLayoutConstraint!
var label3YConst: NSLayoutConstraint!
var label4YConst: NSLayoutConstraint!
var label5YConst: NSLayoutConstraint!
var drawClockwise: Bool = true
var slices: [Slice]?
var sliceIndex: Int = 0
var currentPercent: CGFloat = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
if canvasView == nil {
let container = UIView()
addSubview(container)
canvasView = UIView()
container.addSubview(canvasView)
canvasView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
canvasView.topAnchor.constraint(equalTo: container.topAnchor),
canvasView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
canvasView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
canvasView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
canvasView.backgroundColor = .yellow
label1 = UILabel()
label2 = UILabel()
label3 = UILabel()
label4 = UILabel()
label5 = UILabel()
[label1, label2, label3, label4, label5].forEach {
guard let v = [=12=] else { fatalError("Bad Setup!") }
v.translatesAutoresizingMaskIntoConstraints = false
v.textColor = .white
v.textAlignment = .center
addSubview(v)
}
label1XConst = label1.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label1YConst = label1.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label2XConst = label2.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label2YConst = label2.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label3XConst = label3.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label3YConst = label3.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label4XConst = label4.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label4YConst = label4.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label5XConst = label5.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label5YConst = label5.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
[label1XConst, label2XConst, label3XConst, label4XConst, label5XConst,
label1YConst, label2YConst, label3YConst, label4YConst, label5YConst].forEach {
[=12=]?.isActive = true
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
subviews[0].frame = bounds
}
// don't do this
//override func draw(_ rect: CGRect) {
// subviews[0].frame = bounds
//}
// MARK: Changes end here
/// Get an animation duration for the passed slice.
/// If slice share is 40%, for example, it returns 40% of total animation duration.
///
/// - Parameter slice: Slice struct
/// - Returns: Animation duration
func getDuration(_ slice: Slice) -> CFTimeInterval {
return CFTimeInterval(slice.percent / 1.0 * PieChartView.ANIMATION_DURATION)
}
/// Convert slice percent to radian.
///
/// - Parameter percent: Slice percent (0.0 - 1.0).
/// - Returns: Radian
func percentToRadian(_ percent: CGFloat) -> CGFloat {
//Because angle starts wtih X positive axis, add 270 degrees to rotate it to Y positive axis.
var angle = 270 + percent * 360
if angle >= 360 {
angle -= 360
}
return angle * CGFloat.pi / 180.0
}
/// Add a slice CAShapeLayer to the canvas.
///
/// - Parameter slice: Slice to be drawn.
func addSlice(_ slice: Slice) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = getDuration(slice)
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.delegate = self
let canvasWidth = canvasView.frame.width
let toPercent = currentPercent + (drawClockwise ? slice.percent : -slice.percent)
let path = UIBezierPath(arcCenter: canvasView.center,
radius: canvasWidth * 3 / 8,
startAngle: percentToRadian(currentPercent),
endAngle: percentToRadian(toPercent),
clockwise: drawClockwise)
let sliceLayer = CAShapeLayer()
sliceLayer.path = path.cgPath
sliceLayer.fillColor = nil
sliceLayer.strokeColor = slice.color.cgColor
sliceLayer.lineWidth = canvasWidth * 2 / 8
sliceLayer.strokeEnd = 1
sliceLayer.add(animation, forKey: animation.keyPath)
canvasView.layer.addSublayer(sliceLayer)
}
/// Get label's center position based on from and to percentages.
/// This is always relative to canvasView's center.
///
/// - Parameters:
/// - fromPercent: End of previous slice.
/// - toPercent: End of current slice.
/// - Returns: Center point for label.
func getLabelCenter(_ fromPercent: CGFloat, _ toPercent: CGFloat) -> CGPoint {
let radius = canvasView.frame.width * 3 / 8
let labelAngle = percentToRadian((toPercent - fromPercent) / 2 + fromPercent)
let path = UIBezierPath(arcCenter: canvasView.center,
radius: radius,
startAngle: labelAngle,
endAngle: labelAngle,
clockwise: drawClockwise)
path.close()
return path.currentPoint
}
/// Re-position and draw label such as "43%".
///
/// - Parameter slice: Slice whose label is drawn.
func addLabel(_ slice: Slice) {
let center = canvasView.center
let labelCenter = getLabelCenter(currentPercent, currentPercent + (drawClockwise ? slice.percent : -slice.percent))
let xConst = [label1XConst, label2XConst, label3XConst, label4XConst, label5XConst][sliceIndex]
let yConst = [label1YConst, label2YConst, label3YConst, label4YConst, label5YConst][sliceIndex]
xConst?.constant = labelCenter.x - center.x
yConst?.constant = labelCenter.y - center.y
let label = [label1, label2, label3, label4, label5][sliceIndex]
label?.isHidden = true
label?.text = String(format: "%d%%", Int(slice.percent * 100))
}
/// Call this to start pie chart animation.
func animateChart() {
sliceIndex = 0
currentPercent = 0.0
canvasView.layer.sublayers = nil
if slices != nil && slices!.count > 0 {
let firstSlice = slices![0]
addLabel(firstSlice)
addSlice(firstSlice)
}
}
}
extension MyPieChartView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if flag {
currentPercent += (drawClockwise ? slices![sliceIndex].percent : -slices![sliceIndex].percent)
sliceIndex += 1
if sliceIndex < slices!.count {
let nextSlice = slices![sliceIndex]
addLabel(nextSlice)
addSlice(nextSlice)
} else {
//After animation is done, display all labels. Can be animated.
for label in [label1, label2, label3, label4, label5] {
label?.isHidden = false
}
}
}
}
}
示例:
class ViewController: UIViewController {
@IBOutlet var pieChartView: MyPieChartView!
@IBOutlet var antiPieChartView: MyPieChartView!
override func viewDidLoad() {
super.viewDidLoad()
pieChartView.slices = [
Slice(percent: 0.4, color: UIColor.red),
Slice(percent: 0.3, color: UIColor.blue),
Slice(percent: 0.2, color: UIColor.purple),
Slice(percent: 0.1, color: UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0))
]
antiPieChartView.slices = [
Slice(percent: 0.4, color: UIColor.red),
Slice(percent: 0.3, color: UIColor.blue),
Slice(percent: 0.2, color: UIColor.purple),
Slice(percent: 0.1, color: UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0))
]
// draw this pie anti-clockwise
antiPieChartView.drawClockwise = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
pieChartView.animateChart()
antiPieChartView.animateChart()
}
}