【问题标题】:Multiple CAShapeLayers with .round lineCap are overlapped by each other带有 .round lineCap 的多个 CAShapeLayer 相互重叠
【发布时间】:2022-01-23 23:25:13
【问题描述】:

我尝试使用.round lineCap 和半圆路径组合多个层:

let layer = CAShapeLayer()
layer.lineWidth = 12
layer.lineCap = .round
layer.strokeColor = color.withAlphaComponent(0.32).cgColor
layer.fillColor = UIColor.clear.cgColor
// angles calculation
let path = UIBezierPath(arcCenter: arcCenter,
                        radius: radius,
                        startAngle: startAngle,
                        endAngle: engAngle,
                        clockwise: true)
layer.path = path.cgPath

努力实现以下目标:

但我的图层相互重叠。是否可以以某种方式快速修复它,还是我需要手动实现带圆角的路径计算?

【问题讨论】:

    标签: ios swift uikit calayer


    【解决方案1】:

    .lineCap = .round 时,我们得到一个以端点为中心的“圆”,半径为线宽的 1/2:

    所以,要让圆圆端坐在端点上,我们可以通过asin(lineWidth * 0.5 / radius)调整端点:

    假设我们沿顺时针方向前进:

    let delta: CGFloat = lineCap == .round ? asin(lineWidth * 0.5 / radius) : 0.0
    
    let path = UIBezierPath(arcCenter: center,
                            radius: radius,
                            startAngle: startDegrees.radians + delta,
                            endAngle: endDegrees.radians - delta,
                            clockwise: true)
    

    所以,如果我们有一系列度数用.lineEnd = .butt(默认)形成这个图像:

    我们可以通过偏移开始和结束角度来得到这个:

    这是一个完整的示例类:

    class ConnectedArcsView: UIView {
        
        public var segmentDegrees: [CGFloat] = [] {
            didSet {
                var n: Int = 0
                if let subs = layer.sublayers {
                    n = subs.count
                    if n > segmentDegrees.count {
                        // if we already have sublayers,
                        //  remove any extras
                        for _ in 0..<n - segmentDegrees.count {
                            subs.last?.removeFromSuperlayer()
                        }
                    }
                }
                // add sublayers if needed
                while n <  segmentDegrees.count {
                    let l = CAShapeLayer()
                    l.fillColor = UIColor.clear.cgColor
                    layer.addSublayer(l)
                    n += 1
                }
                setNeedsLayout()
            }
        }
        
        // segment colors default: [.red, .green, .blue]
        public var segmentColors: [UIColor] = [.red, .green, .blue] { didSet { setNeedsLayout() } }
        
        // line width default: 12
        public var lineWidth: CGFloat = 12 { didSet { setNeedsLayout() } }
        
        // line cap default: .round
        public var lineCap: CAShapeLayerLineCap = .round  { didSet { setNeedsLayout() } }
        
        override func layoutSubviews() {
            super.layoutSubviews()
            
            guard let subs = layer.sublayers else { return }
            
            let radius = (bounds.size.width - lineWidth) * 0.5
            let center = CGPoint(x: bounds.midX, y: bounds.midY)
            
            // if lineCap == .round
            //  calculate delta for start and end angles
            let delta: CGFloat = lineCap == .round ? asin(lineWidth * 0.5 / radius) : 0.0
            
            // calculate start angle so the "gap" is centered at the bottom
            let totalDegrees: CGFloat = segmentDegrees.reduce(0.0, +)
            var startDegrees: CGFloat = 90.0 + (360.0 - totalDegrees) * 0.5
            
            for i in 0..<segmentDegrees.count {
                let endDegrees = startDegrees + segmentDegrees[i]
                
                guard let shape = subs[i] as? CAShapeLayer else { continue }
                
                shape.lineWidth = lineWidth
                shape.lineCap = lineCap
                shape.strokeColor = segmentColors[i % segmentColors.count].cgColor
                
                let path = UIBezierPath(arcCenter: center,
                                        radius: radius,
                                        startAngle: startDegrees.radians + delta,
                                        endAngle: endDegrees.radians - delta,
                                        clockwise: true)
                
                shape.path = path.cgPath
                
                startDegrees += segmentDegrees[i]
            }
            
        }
    
    }
    

    以及显示其用法的示例视图控制器:

    class ExampleViewController: UIViewController {
        
        let testView = ConnectedArcsView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemBlue
            
            let degrees: [CGFloat] = [
                40, 25, 140, 25, 40
            ]
            
            let colors: [UIColor] = [
                .systemRed, .systemYellow, .systemGreen, .systemYellow, .systemRed
            ] //.map { ($0 as UIColor).withAlphaComponent(0.5) }
            
            testView.segmentDegrees = degrees
            testView.segmentColors = colors
            testView.lineWidth = 12
            
            testView.backgroundColor = .black
            testView.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(testView)
            
            // add an info label
            let v = UILabel()
            v.textAlignment = .center
            v.numberOfLines = 0
            v.text = "Tap anywhere to toggle between\n\".round\" and \".butt\""
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                
                testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
                testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
                testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
                testView.heightAnchor.constraint(equalTo: testView.widthAnchor),
                
                v.topAnchor.constraint(equalTo: testView.bottomAnchor, constant: 20.0),
                v.widthAnchor.constraint(equalTo: testView.widthAnchor),
                v.centerXAnchor.constraint(equalTo: testView.centerXAnchor),
                
            ])
            
        }
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            testView.lineCap = testView.lineCap == .round ? .butt : .round
        }
        
    }
    

    运行时,点击任意位置将在.round.butt 之间切换。


    编辑 - 忘记包含辅助扩展:

    extension CGFloat {
        var degrees: CGFloat {
            return self * CGFloat(180) / .pi
        }
        var radians: CGFloat {
            return self * .pi / 180.0
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-11-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-08
      相关资源
      最近更新 更多