简体   繁体   English

如何在不同的 CALayer 之间提供保持间距

[英]How to provide maintain spacing between different CALayers

Heading ##I'm trying to learn the charts and having trouble标题##我正在尝试学习图表并且遇到了麻烦

  1. Adding consistent space between the slices.在切片之间添加一致的空间。
  2. Start the animation in sequence.依次启动 animation。

The reason, I didn't want the separator as a separate arch is to have both the edges rounded .原因,我不希望分隔符作为一个单独的拱门是让两个边缘都变圆 Adding a separator as another layer overlaps the rounded corners.添加分隔符作为另一层与圆角重叠。 在此处输入图像描述

Any help or pointers is highly appreciated.非常感谢任何帮助或指示。

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 $0 + $1.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)

UPDATED:更新:

Updated the code to include proper spacing irrespective of single/multiple items on the chart with equal distribution of total spacing, based on a suggestion from @jaferAli根据@jaferAli 的建议,更新了代码以包含适当的间距,而不考虑图表上的单个/多个项目,总间距的分布相等

Open Issue: Handling tap gesture on the layer so I can perform custom actions based on the category selected.未解决的问题:处理图层上的点击手势,以便我可以根据所选类别执行自定义操作。

Screen 2屏幕 2 在此处输入图像描述

UPDATED CODE:更新代码:

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 $0 + $1.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)

You need to calculate spacing and then add and Subtract it from start and end angle.. so update your calcualteStartAndEndAngle Method with this one您需要计算间距,然后从开始和结束角度添加和减去它。所以用这个更新您的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 $0 + $1.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
    }

It will Result this Animation它将导致此 Animation

在此处输入图像描述

and if you want linear Animation then change your animation method如果你想要线性 Animation 然后改变你的 animation 方法

 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)
    }

在此处输入图像描述

And if you want to learn more you can see this framework RingPieChart如果您想了解更多信息,可以查看此框架RingPieChart

The problem was 1. Re - Calculate the percentages by keeping the spacing percentage.问题是 1. 重新 - 通过保持间距百分比来计算百分比。

that is,那是,

    //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
    }

  1. Once this is done, the total categories + spacings will equate to 100 %.完成此操作后,总类别 + 间距将等于 100%。

  2. The only problem left is, for very smaller percentage categories (lesser than spacing percentage), the start angle will be greater than end angle.剩下的唯一问题是,对于非常小的百分比类别(小于间距百分比),开始角度将大于结束角度。 This is because we are subtracting the spacing from end angle.这是因为我们从端角中减去间距。 there are two options to correct, a.有两个选项可以纠正,a。 flip the angles.翻转角度。


            if angle.start > angle.end {
                let start = angle.start
                angle.start = angle.end
                angle.end = start
            }

b.湾。 draw it anti clock wise in Beizer path, only for that slice.在 Beizer 路径中逆时针绘制,仅针对该切片。

        let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: angle.start, endAngle: angle.end, clockwise: **angle.start < angle.end**)

this should solve all the problems, i will upload my findings on a GIT repo and publish the link here.这应该可以解决所有问题,我会将我的发现上传到 GIT 存储库并在此处发布链接。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM