如何保持不同 CALayer 之间的间距
How to provide maintain spacing between different CALayers
标题 ##我正在尝试学习图表但遇到了问题
- 在切片之间添加一致的 space。
- 按顺序开始播放动画。
原因是,我不想将分隔符作为一个单独的拱门,因为它的两个边都圆角。添加分隔符作为另一层重叠圆角。
非常感谢任何帮助或指点。
import UIKit
import PlaygroundSupport
var str = "Hello, playground"
struct dataItem {
var color: UIColor
var percentage: CGFloat
}
typealias pieAngle = (start: CGFloat, end: CGFloat, color: UIColor)
let pieDataToDisplay = [dataItem(color: .red, percentage: 10),
dataItem(color: .blue, percentage: 20),
dataItem(color: .green, percentage: 25),
dataItem(color: .yellow, percentage: 25),
dataItem(color: .orange, percentage: 10)]
class USBCircleChart: UIView {
private var piesToDisplay: [dataItem] = [] { didSet { setNeedsLayout() } }
private var seperatorSpace: Double = 2.0 { didSet { setNeedsLayout() } }
func fillDataForChart(with items: [dataItem] ) {
self.piesToDisplay.append(contentsOf: items)
print("getting data \(self.piesToDisplay)")
layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
guard piesToDisplay.count > 0 else { return }
print("laying out data")
let angles = calcualteStartAndEndAngle(items: piesToDisplay)
for i in angles {
var dataItem = i
addSpace(data: &dataItem)
addShapeToCircle(data: dataItem)
}
}
func addSpace(data:inout pieAngle) -> pieAngle {
// If space is not added, then its collated at the end, we have to scatter it between each item.
//data.end -= CGFloat(seperatorSpace)
return data
}
func addShapeToCircle(data : pieAngle, percent: CGFloat) {
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
var shapeLayer = CAShapeLayer()
// radians = degrees * pi / 180
// x*2 + y*2 = r*2
//cos teta = x/r --> x = r * cos teta
// sinn teta = y/ r --> y = r * sin teta
// let x = 100 * cos(data.start)
// let y = 100 * sin(data.end)
let radius = (bounds.origin.x + bounds.size.width / 2 - (sliceThickness)) / 2
//This is the circle path drawn.
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.frame.width / 2, startAngle: data.start, endAngle: data.end, clockwise: true) //2*CGFloat.pi
shapeLayer.path = circularPath.cgPath
//Provide a bounding box for the shape layer to handle events
//Removing the below line works but will not handle touch events :(
shapeLayer.bounds = circularPath.cgPath.boundingBox
//Start the angle from anyplace you need { + - of Pi} // {0, 0.5 pi, 1 pi, 1.5pi}
// shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2 , 0, 0, 1)
// color of the stroke
shapeLayer.strokeColor = data.color.cgColor
//Width of stoke
shapeLayer.lineWidth = sliceThickness
//Starts from the center of the view
shapeLayer.position = center
//To provide a rounded cap on the stroke
shapeLayer.lineCap = .round
//Fills the entire circle with this color
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
basicAnim(shapeLayer: &shapeLayer, percentage: percent)
layer.addSublayer(shapeLayer)
}
func basicAnim(shapeLayer: inout CAShapeLayer) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 10
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
}
// //Calucate percentage based on given values
// public func calculateAngle(percentageVal:Double)-> CGFloat {
// return CGFloat((percentageVal / 100) * 360)
// let val = CGFloat (percentageVal / 100.0)
// return val * 2 * CGFloat.pi
// }
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
var totalSeperatorSpace = Double(items.count) * separatorSpace
var totalSum = items.reduce(CGFloat(totalSeperatorSpace)) { return [=10=] + .percentage }
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * .pi) + angleToStart
angle.0 = angleToStart
angle.1 = endAngle
angle.2 = item.color
angleList.append(angle)
angleToStart = endAngle
//print(angle)
}
return angleList
}
}
let container = UIView()
container.frame.size = CGSize(width: 360, height: 360)
container.backgroundColor = .white
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
let m = USBCircleChart(frame: CGRect(x: 0, y: 0, width: 215, height: 215))
m.center = CGPoint(x: container.bounds.size.width / 2, y: container.bounds.size.height / 2)
m.fillDataForChart(with: pieDataToDisplay)
container.addSubview(m)
更新:
根据 @jaferAli
的建议,更新了代码以包含适当的间距,而不考虑图表上的 single/multiple 个项目,总间距的平均分布
未决问题: 处理图层上的点击手势,以便我可以根据所选类别执行自定义操作。
屏幕 2
更新代码:
import UIKit
import PlaygroundSupport
var str = "Hello, playground"
struct dataItem {
var color: UIColor
var percentage: CGFloat
}
func hexStringToUIColor (hex:String) -> UIColor {
var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if (cString.hasPrefix("#")) {
cString.remove(at: cString.startIndex)
}
if ((cString.count) != 6) {
return UIColor.gray
}
var rgbValue:UInt64 = 0
Scanner(string: cString).scanHexInt64(&rgbValue)
return UIColor(
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
alpha: CGFloat(1.0)
)
}
typealias pieAngle = (start: CGFloat, end: CGFloat, color: UIColor, percent: CGFloat)
let pieDataToDisplay = [
dataItem(color: hexStringToUIColor(hex: "#E61628"), percentage: 10),
dataItem(color: hexStringToUIColor(hex: "#50B7FB"), percentage: 20),
dataItem(color: hexStringToUIColor(hex: "#38BE72"), percentage: 25),
dataItem(color: hexStringToUIColor(hex: "#FFAA4C"), percentage: 15),
dataItem(color: hexStringToUIColor(hex: "#B6BE33"), percentage: 30)
]
let pieDataToDisplayWhite = [dataItem(color: .white, percentage: 10),
dataItem(color: .white, percentage: 20),
dataItem(color: .white, percentage: 25),
dataItem(color: .white, percentage: 25),
dataItem(color: .orange, percentage: 10)]
class USBCircleChart: UIView {
private var piesToDisplay: [dataItem] = [] { didSet { setNeedsLayout() } }
private var seperatorSpace: Double = 5.0 { didSet { setNeedsLayout() } }
private var sliceThickness: CGFloat = 10.0 { didSet { setNeedsLayout() } }
func fillDataForChart(with items: [dataItem] ) {
self.piesToDisplay.append(contentsOf: items)
print("getting data \(self.piesToDisplay)")
layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
guard piesToDisplay.count > 0 else { return }
print("laying out data")
let angles = calcualteStartAndEndAngle(items: piesToDisplay)
for i in angles {
var dataItem = i
addSpace(data: &dataItem)
addShapeToCircle(data: dataItem, percent:i.percent)
}
}
func addSpace(data:inout pieAngle) -> pieAngle {
// If space is not added, then its collated at the end, we have to scatter it between each item.
//data.end -= CGFloat(seperatorSpace)
return data
}
func addShapeToCircle(data : pieAngle, percent: CGFloat) {
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
var shapeLayer = CAShapeLayer()
// radians = degrees * pi / 180
// x*2 + y*2 = r*2
//cos teta = x/r --> x = r * cos teta
// sinn teta = y/ r --> y = r * sin teta
// let x = 100 * cos(data.start)
// let y = 100 * sin(data.end)
let radius = (bounds.origin.x + bounds.size.width / 2 - (sliceThickness)) / 2
//This is the circle path drawn.
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.frame.width / 2, startAngle: data.start, endAngle: data.end, clockwise: true) //2*CGFloat.pi
shapeLayer.path = circularPath.cgPath
//Provide a bounding box for the shape layer to handle events
//shapeLayer.bounds = circularPath.cgPath.boundingBox
//Start the angle from anyplace you need { + - of Pi} // {0, 0.5 pi, 1 pi, 1.5pi}
// shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2 , 0, 0, 1)
// color of the stroke
shapeLayer.strokeColor = data.color.cgColor
//Width of stoke
shapeLayer.lineWidth = sliceThickness
//Starts from the center of the view
shapeLayer.position = center
//To provide a rounded cap on the stroke
shapeLayer.lineCap = .round
//Fills the entire circle with this color
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
basicAnim(shapeLayer: &shapeLayer, percentage: percent)
layer.addSublayer(shapeLayer)
}
func basicAnim(shapeLayer: inout CAShapeLayer) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 10
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
}
private var timeOffset:CFTimeInterval = 0
func basicAnim(shapeLayer: inout CAShapeLayer, percentage:CGFloat) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(percentage / 50)
basicAnimation.beginTime = CACurrentMediaTime() + timeOffset
print("timeOffset:\(timeOffset),")
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
timeOffset += CFTimeInterval(percentage / 50)
}
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
let totalSeperatorSpace = Double(items.count)
let totalSum = items.reduce(CGFloat(seperatorSpace)) { return [=11=] + .percentage }
let spacing = CGFloat(seperatorSpace ) / CGFloat (totalSum)
print("total Sum:\(spacing)")
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * CGFloat.pi) + angleToStart
print("start:\(angleToStart) end:\(endAngle)")
angle.0 = angleToStart + spacing
angle.1 = endAngle - spacing
angle.2 = item.color
angle.3 = item.percentage
angleList.append(angle)
angleToStart = endAngle + spacing
//print(angle)
}
return angleList
}
}
extension USBCircleChart {
@objc func handleTap() {
print("getting tap action")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
guard let loca = touch?.location(in: self) else { return }
let point = self.convert(loca, from: nil)
guard let sublayers = self.layer.sublayers as? [CAShapeLayer] else { return }
for layer in sublayers {
print("checking paths \(point) \(loca) \(layer.path) \n")
if let path = layer.path, path.contains(point) {
print(layer)
}
}
}
}
let container = UIView()
container.frame.size = CGSize(width: 300, height: 300)
container.backgroundColor = .white
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
let m = USBCircleChart(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
//m.center = CGPoint(x: container.bounds.size.width / 2, y: container.bounds.size.height / 2)
m.center = container.center
m.fillDataForChart(with: pieDataToDisplay)
container.addSubview(m)
您需要计算间距,然后从开始角度和结束角度添加和减去它。所以用这个
更新您的 calcualteStartAndEndAngle
方法
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
let totalSeperatorSpace = Double(items.count)
let totalSum = items.reduce(CGFloat(totalSeperatorSpace)) { return [=10=] + .percentage }
let spacing = CGFloat( totalSeperatorSpace + 1 ) / totalSum
print("total Sum:\(spacing)")
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * .pi) + angleToStart
print("start:\(angleToStart) end:\(endAngle)")
angle.0 = angleToStart + spacing
angle.1 = endAngle - spacing
angle.2 = item.color
angleList.append(angle)
angleToStart = endAngle + spacing
//print(angle)
}
return angleList
}
它将产生这个动画
如果你想要线性动画,那么改变你的动画方法
private var timeOffset:CFTimeInterval = 0
func basicAnim(shapeLayer: inout CAShapeLayer, percentage:CGFloat) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(percentage)
basicAnimation.beginTime = CACurrentMediaTime() + timeOffset
print("timeOffset:\(timeOffset),")
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
timeOffset += CFTimeInterval(percentage)
}
如果您想了解更多信息,可以查看此框架 RingPieChart
问题是
1.重新计算保持间距百分比的百分比
即
//This is to recalculate the percentage by adding the total spacing percentage.
/// Example : The percenatge of each category is recalculated - for instance , lets assume Apple - 60 %,
/// Android - 40 %, now we add Samsung as 10 %, which equates to 110%, To correct this
/// Apple 60 * (100- Samsung) / 100 = 54 %, Android = 36 %, which totals to 100 %.
///
/// - Parameter buffer: total spacing between the splices.
func updatedPercentage(with buffer: CGFloat ) -> CGFloat {
return percentage * (100 - buffer) / 100
}
完成后,类别总数 + 间距将等于 100%。
剩下的唯一问题是,对于非常小的百分比类别(小于间距百分比),起始角度将大于结束角度。这是因为我们要从端角中减去间距。
有两个选项可以更正,
一种。翻转角度。
if angle.start > angle.end {
let start = angle.start
angle.start = angle.end
angle.end = start
}
b。在 Beizer 路径中逆时针绘制它,仅针对该切片。
let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: angle.start, endAngle: angle.end, clockwise: **angle.start < angle.end**)
这应该可以解决所有问题,我会将我的发现上传到 GIT 存储库并在此处发布 link。
标题 ##我正在尝试学习图表但遇到了问题
- 在切片之间添加一致的 space。
- 按顺序开始播放动画。
原因是,我不想将分隔符作为一个单独的拱门,因为它的两个边都圆角。添加分隔符作为另一层重叠圆角。
非常感谢任何帮助或指点。
import UIKit
import PlaygroundSupport
var str = "Hello, playground"
struct dataItem {
var color: UIColor
var percentage: CGFloat
}
typealias pieAngle = (start: CGFloat, end: CGFloat, color: UIColor)
let pieDataToDisplay = [dataItem(color: .red, percentage: 10),
dataItem(color: .blue, percentage: 20),
dataItem(color: .green, percentage: 25),
dataItem(color: .yellow, percentage: 25),
dataItem(color: .orange, percentage: 10)]
class USBCircleChart: UIView {
private var piesToDisplay: [dataItem] = [] { didSet { setNeedsLayout() } }
private var seperatorSpace: Double = 2.0 { didSet { setNeedsLayout() } }
func fillDataForChart(with items: [dataItem] ) {
self.piesToDisplay.append(contentsOf: items)
print("getting data \(self.piesToDisplay)")
layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
guard piesToDisplay.count > 0 else { return }
print("laying out data")
let angles = calcualteStartAndEndAngle(items: piesToDisplay)
for i in angles {
var dataItem = i
addSpace(data: &dataItem)
addShapeToCircle(data: dataItem)
}
}
func addSpace(data:inout pieAngle) -> pieAngle {
// If space is not added, then its collated at the end, we have to scatter it between each item.
//data.end -= CGFloat(seperatorSpace)
return data
}
func addShapeToCircle(data : pieAngle, percent: CGFloat) {
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
var shapeLayer = CAShapeLayer()
// radians = degrees * pi / 180
// x*2 + y*2 = r*2
//cos teta = x/r --> x = r * cos teta
// sinn teta = y/ r --> y = r * sin teta
// let x = 100 * cos(data.start)
// let y = 100 * sin(data.end)
let radius = (bounds.origin.x + bounds.size.width / 2 - (sliceThickness)) / 2
//This is the circle path drawn.
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.frame.width / 2, startAngle: data.start, endAngle: data.end, clockwise: true) //2*CGFloat.pi
shapeLayer.path = circularPath.cgPath
//Provide a bounding box for the shape layer to handle events
//Removing the below line works but will not handle touch events :(
shapeLayer.bounds = circularPath.cgPath.boundingBox
//Start the angle from anyplace you need { + - of Pi} // {0, 0.5 pi, 1 pi, 1.5pi}
// shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2 , 0, 0, 1)
// color of the stroke
shapeLayer.strokeColor = data.color.cgColor
//Width of stoke
shapeLayer.lineWidth = sliceThickness
//Starts from the center of the view
shapeLayer.position = center
//To provide a rounded cap on the stroke
shapeLayer.lineCap = .round
//Fills the entire circle with this color
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
basicAnim(shapeLayer: &shapeLayer, percentage: percent)
layer.addSublayer(shapeLayer)
}
func basicAnim(shapeLayer: inout CAShapeLayer) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 10
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
}
// //Calucate percentage based on given values
// public func calculateAngle(percentageVal:Double)-> CGFloat {
// return CGFloat((percentageVal / 100) * 360)
// let val = CGFloat (percentageVal / 100.0)
// return val * 2 * CGFloat.pi
// }
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
var totalSeperatorSpace = Double(items.count) * separatorSpace
var totalSum = items.reduce(CGFloat(totalSeperatorSpace)) { return [=10=] + .percentage }
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * .pi) + angleToStart
angle.0 = angleToStart
angle.1 = endAngle
angle.2 = item.color
angleList.append(angle)
angleToStart = endAngle
//print(angle)
}
return angleList
}
}
let container = UIView()
container.frame.size = CGSize(width: 360, height: 360)
container.backgroundColor = .white
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
let m = USBCircleChart(frame: CGRect(x: 0, y: 0, width: 215, height: 215))
m.center = CGPoint(x: container.bounds.size.width / 2, y: container.bounds.size.height / 2)
m.fillDataForChart(with: pieDataToDisplay)
container.addSubview(m)
更新:
根据 @jaferAli
的建议,更新了代码以包含适当的间距,而不考虑图表上的 single/multiple 个项目,总间距的平均分布未决问题: 处理图层上的点击手势,以便我可以根据所选类别执行自定义操作。
屏幕 2
更新代码:
import UIKit
import PlaygroundSupport
var str = "Hello, playground"
struct dataItem {
var color: UIColor
var percentage: CGFloat
}
func hexStringToUIColor (hex:String) -> UIColor {
var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
if (cString.hasPrefix("#")) {
cString.remove(at: cString.startIndex)
}
if ((cString.count) != 6) {
return UIColor.gray
}
var rgbValue:UInt64 = 0
Scanner(string: cString).scanHexInt64(&rgbValue)
return UIColor(
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
alpha: CGFloat(1.0)
)
}
typealias pieAngle = (start: CGFloat, end: CGFloat, color: UIColor, percent: CGFloat)
let pieDataToDisplay = [
dataItem(color: hexStringToUIColor(hex: "#E61628"), percentage: 10),
dataItem(color: hexStringToUIColor(hex: "#50B7FB"), percentage: 20),
dataItem(color: hexStringToUIColor(hex: "#38BE72"), percentage: 25),
dataItem(color: hexStringToUIColor(hex: "#FFAA4C"), percentage: 15),
dataItem(color: hexStringToUIColor(hex: "#B6BE33"), percentage: 30)
]
let pieDataToDisplayWhite = [dataItem(color: .white, percentage: 10),
dataItem(color: .white, percentage: 20),
dataItem(color: .white, percentage: 25),
dataItem(color: .white, percentage: 25),
dataItem(color: .orange, percentage: 10)]
class USBCircleChart: UIView {
private var piesToDisplay: [dataItem] = [] { didSet { setNeedsLayout() } }
private var seperatorSpace: Double = 5.0 { didSet { setNeedsLayout() } }
private var sliceThickness: CGFloat = 10.0 { didSet { setNeedsLayout() } }
func fillDataForChart(with items: [dataItem] ) {
self.piesToDisplay.append(contentsOf: items)
print("getting data \(self.piesToDisplay)")
layoutIfNeeded()
}
override func layoutSubviews() {
super.layoutSubviews()
guard piesToDisplay.count > 0 else { return }
print("laying out data")
let angles = calcualteStartAndEndAngle(items: piesToDisplay)
for i in angles {
var dataItem = i
addSpace(data: &dataItem)
addShapeToCircle(data: dataItem, percent:i.percent)
}
}
func addSpace(data:inout pieAngle) -> pieAngle {
// If space is not added, then its collated at the end, we have to scatter it between each item.
//data.end -= CGFloat(seperatorSpace)
return data
}
func addShapeToCircle(data : pieAngle, percent: CGFloat) {
let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
var shapeLayer = CAShapeLayer()
// radians = degrees * pi / 180
// x*2 + y*2 = r*2
//cos teta = x/r --> x = r * cos teta
// sinn teta = y/ r --> y = r * sin teta
// let x = 100 * cos(data.start)
// let y = 100 * sin(data.end)
let radius = (bounds.origin.x + bounds.size.width / 2 - (sliceThickness)) / 2
//This is the circle path drawn.
let circularPath = UIBezierPath(arcCenter: .zero, radius: self.frame.width / 2, startAngle: data.start, endAngle: data.end, clockwise: true) //2*CGFloat.pi
shapeLayer.path = circularPath.cgPath
//Provide a bounding box for the shape layer to handle events
//shapeLayer.bounds = circularPath.cgPath.boundingBox
//Start the angle from anyplace you need { + - of Pi} // {0, 0.5 pi, 1 pi, 1.5pi}
// shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2 , 0, 0, 1)
// color of the stroke
shapeLayer.strokeColor = data.color.cgColor
//Width of stoke
shapeLayer.lineWidth = sliceThickness
//Starts from the center of the view
shapeLayer.position = center
//To provide a rounded cap on the stroke
shapeLayer.lineCap = .round
//Fills the entire circle with this color
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeEnd = 0
basicAnim(shapeLayer: &shapeLayer, percentage: percent)
layer.addSublayer(shapeLayer)
}
func basicAnim(shapeLayer: inout CAShapeLayer) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 10
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
}
private var timeOffset:CFTimeInterval = 0
func basicAnim(shapeLayer: inout CAShapeLayer, percentage:CGFloat) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(percentage / 50)
basicAnimation.beginTime = CACurrentMediaTime() + timeOffset
print("timeOffset:\(timeOffset),")
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
timeOffset += CFTimeInterval(percentage / 50)
}
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
let totalSeperatorSpace = Double(items.count)
let totalSum = items.reduce(CGFloat(seperatorSpace)) { return [=11=] + .percentage }
let spacing = CGFloat(seperatorSpace ) / CGFloat (totalSum)
print("total Sum:\(spacing)")
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * CGFloat.pi) + angleToStart
print("start:\(angleToStart) end:\(endAngle)")
angle.0 = angleToStart + spacing
angle.1 = endAngle - spacing
angle.2 = item.color
angle.3 = item.percentage
angleList.append(angle)
angleToStart = endAngle + spacing
//print(angle)
}
return angleList
}
}
extension USBCircleChart {
@objc func handleTap() {
print("getting tap action")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
guard let loca = touch?.location(in: self) else { return }
let point = self.convert(loca, from: nil)
guard let sublayers = self.layer.sublayers as? [CAShapeLayer] else { return }
for layer in sublayers {
print("checking paths \(point) \(loca) \(layer.path) \n")
if let path = layer.path, path.contains(point) {
print(layer)
}
}
}
}
let container = UIView()
container.frame.size = CGSize(width: 300, height: 300)
container.backgroundColor = .white
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true
let m = USBCircleChart(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
//m.center = CGPoint(x: container.bounds.size.width / 2, y: container.bounds.size.height / 2)
m.center = container.center
m.fillDataForChart(with: pieDataToDisplay)
container.addSubview(m)
您需要计算间距,然后从开始角度和结束角度添加和减去它。所以用这个
更新您的calcualteStartAndEndAngle
方法
private func calcualteStartAndEndAngle(items : [dataItem])-> [pieAngle] {
var angle: pieAngle
var angleToStart: CGFloat = 0.0
//Add the total separator space to the circle so we can accurately measure the start point with space.
let totalSeperatorSpace = Double(items.count)
let totalSum = items.reduce(CGFloat(totalSeperatorSpace)) { return [=10=] + .percentage }
let spacing = CGFloat( totalSeperatorSpace + 1 ) / totalSum
print("total Sum:\(spacing)")
var angleList: [pieAngle] = []
for item in items {
//Find the end angle based on the percentage in the total circle
let endAngle = (item.percentage / totalSum * 2 * .pi) + angleToStart
print("start:\(angleToStart) end:\(endAngle)")
angle.0 = angleToStart + spacing
angle.1 = endAngle - spacing
angle.2 = item.color
angleList.append(angle)
angleToStart = endAngle + spacing
//print(angle)
}
return angleList
}
它将产生这个动画
如果你想要线性动画,那么改变你的动画方法
private var timeOffset:CFTimeInterval = 0
func basicAnim(shapeLayer: inout CAShapeLayer, percentage:CGFloat) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(percentage)
basicAnimation.beginTime = CACurrentMediaTime() + timeOffset
print("timeOffset:\(timeOffset),")
//Forwards will hold the layer after completion
basicAnimation.fillMode = .forwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "shapeLayerAniamtion")
timeOffset += CFTimeInterval(percentage)
}
如果您想了解更多信息,可以查看此框架 RingPieChart
问题是 1.重新计算保持间距百分比的百分比
即
//This is to recalculate the percentage by adding the total spacing percentage.
/// Example : The percenatge of each category is recalculated - for instance , lets assume Apple - 60 %,
/// Android - 40 %, now we add Samsung as 10 %, which equates to 110%, To correct this
/// Apple 60 * (100- Samsung) / 100 = 54 %, Android = 36 %, which totals to 100 %.
///
/// - Parameter buffer: total spacing between the splices.
func updatedPercentage(with buffer: CGFloat ) -> CGFloat {
return percentage * (100 - buffer) / 100
}
完成后,类别总数 + 间距将等于 100%。
剩下的唯一问题是,对于非常小的百分比类别(小于间距百分比),起始角度将大于结束角度。这是因为我们要从端角中减去间距。 有两个选项可以更正, 一种。翻转角度。
if angle.start > angle.end {
let start = angle.start
angle.start = angle.end
angle.end = start
}
b。在 Beizer 路径中逆时针绘制它,仅针对该切片。
let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: angle.start, endAngle: angle.end, clockwise: **angle.start < angle.end**)
这应该可以解决所有问题,我会将我的发现上传到 GIT 存储库并在此处发布 link。