ios - 带有 CAShapelayer 的圆弧圆环图 - 底层的边界可见

标签 ios calayer cashapelayer

我用 CAShapeLayers 弧线绘制了一个圆环图。我画了enter image description here通过将一个放在另一个之上以及下面层边缘可见的问题。

绘图代码如下

for (index, item) in values.enumerated() {
            var currentValue = previousValue + item.value
            previousValue = currentValue
            if index == values.count - 1 {
                currentValue = 100
            }

            let layer = CAShapeLayer()
            let path = UIBezierPath()

            let separatorLayer = CAShapeLayer()
            let separatorPath = UIBezierPath()

            let radius: CGFloat = self.frame.width / 2 - lineWidth / 2
            let center: CGPoint = CGPoint(x: self.bounds.width / 2, y: self.bounds.width / 2)

            separatorPath.addArc(withCenter: center, radius: radius, startAngle: percentToRadians(percent: -25), endAngle: percentToRadians(percent: CGFloat(currentValue - 25 + 0.2)), clockwise: true)
            separatorLayer.path = separatorPath.cgPath
            separatorLayer.fillColor = UIColor.clear.cgColor
            separatorLayer.strokeColor = UIColor.white.cgColor
            separatorLayer.lineWidth = lineWidth
            separatorLayer.contentsScale = UIScreen.main.scale
            self.layer.addSublayer(separatorLayer)
            separatorLayer.add(createGraphAnimation(), forKey: nil)
            separatorLayer.zPosition = -(CGFloat)(index)

            path.addArc(withCenter: center, radius: radius, startAngle: percentToRadians(percent: -25), endAngle: percentToRadians(percent: CGFloat(currentValue - 25)), clockwise: true)
            layer.path = path.cgPath
            layer.fillColor = UIColor.clear.cgColor
            layer.strokeColor = item.color.cgColor
            layer.lineWidth = lineWidth
            layer.contentsScale = UIScreen.main.scale
            layer.shouldRasterize = true
            layer.rasterizationScale = UIScreen.main.scale
            layer.allowsEdgeAntialiasing = true
            separatorLayer.addSublayer(layer)
            layer.add(createGraphAnimation(), forKey: nil)
            layer.zPosition = -(CGFloat)(index)

我究竟做错了什么 ?

UPD

试过的代码
let mask = CAShapeLayer()
        mask.frame = CGRect(x: 0, y: 0, width: radius * 2, height: radius * 2)
        mask.fillColor = nil
        mask.strokeColor = UIColor.white.cgColor
        mask.lineWidth = lineWidth * 2
        let maskPath = CGMutablePath()
        maskPath.addArc(center: CGPoint(x: self.radius, y: self.radius), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
        maskPath.closeSubpath()
        mask.path = maskPath
        self.layer.mask = mask

但它只掩盖了内部边缘,外部仍然有边缘

最佳答案

您看到的边缘发生是因为您在同一位置两次绘制完全相同的形状,而 alpha 合成(通常实现)并不是为了处理这种情况而设计的。 Porter and Duff's paper, “Compositing Digital Images” ,介绍了 alpha 合成,讨论了这个问题:

We must remember that our basic assumption about the division of subpixel areas by geometric objects breaks down in the face of input pictures with correlated mattes. When one picture appears twice in a compositing expression, we must take care with our computations of F A and F B. Those listed in the table are correct only for uncorrelated pictures.



当它说“哑光”时,它基本上意味着透明。当它说“不相关的图片”时,是指透明区域没有特殊关系的两张图片。但在你的情况下,你的两张图片确实有一个特殊的关系:图片在完全相同的区域是透明的!

这是一个可以重现您的问题的独立测试:
private func badVersion() {
    let center = CGPoint(x: view.bounds.width / 2, y: view.bounds.height / 2)
    let radius: CGFloat = 100
    let ringWidth: CGFloat = 44

    let ring = CAShapeLayer()
    ring.frame = view.bounds
    ring.fillColor = nil
    ring.strokeColor = UIColor.red.cgColor
    ring.lineWidth = ringWidth
    let ringPath = CGMutablePath()
    ringPath.addArc(center: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
    ringPath.closeSubpath()
    ring.path = ringPath
    view.layer.addSublayer(ring)

    let wedge = CAShapeLayer()
    wedge.frame = view.bounds
    wedge.fillColor = nil
    wedge.strokeColor = UIColor.darkGray.cgColor
    wedge.lineWidth = ringWidth
    wedge.lineCap = kCALineCapButt
    let wedgePath = CGMutablePath()
    wedgePath.addArc(center: center, radius: radius, startAngle: 0.1, endAngle: 0.6, clockwise: false)
    wedge.path = wedgePath
    view.layer.addSublayer(wedge)
}

这是显示问题的屏幕部分:

the problem

解决此问题的一种方法是将颜色绘制到环边缘之外,并使用蒙版将它们剪辑到环形状。

我将更改我的代码,以便在其顶部绘制一个红色圆盘和一个灰色楔形,而不是绘制一个红色环和灰色环的一部分:

red disc with gray wedge

如果放大,您可以看到这仍然显示灰色楔形边缘的红色条纹。所以诀窍是使用环形蒙版来获得最终形状。这是蒙版的形状,在先前图像的顶部以白色绘制:

white mask

请注意, mask 远离边缘有问题的区域。当我将蒙版用作蒙版而不是绘制它时,我得到了最终的完美结果:

perfect ring

这是绘制完美版本的代码:
private func goodVersion() {
    let center = CGPoint(x: view.bounds.width / 2, y: view.bounds.height / 2)
    let radius: CGFloat = 100
    let ringWidth: CGFloat = 44
    let slop: CGFloat = 10

    let disc = CAShapeLayer()
    disc.frame = view.bounds
    disc.fillColor = UIColor.red.cgColor
    disc.strokeColor = nil
    let ringPath = CGMutablePath()
    ringPath.addArc(center: center, radius: radius + ringWidth / 2 + slop, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
    ringPath.closeSubpath()
    disc.path = ringPath
    view.layer.addSublayer(disc)

    let wedge = CAShapeLayer()
    wedge.frame = view.bounds
    wedge.fillColor = UIColor.darkGray.cgColor
    wedge.strokeColor = nil
    let wedgePath = CGMutablePath()
    wedgePath.move(to: center)
    wedgePath.addArc(center: center, radius: radius + ringWidth / 2 + slop, startAngle: 0.1, endAngle: 0.6, clockwise: false)
    wedgePath.closeSubpath()
    wedge.path = wedgePath
    view.layer.addSublayer(wedge)

    let mask = CAShapeLayer()
    mask.frame = view.bounds
    mask.fillColor = nil
    mask.strokeColor = UIColor.white.cgColor
    mask.lineWidth = ringWidth
    let maskPath = CGMutablePath()
    maskPath.addArc(center: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
    maskPath.closeSubpath()
    mask.path = maskPath
    view.layer.mask = mask
}

请注意,掩码适用于 view 中的所有内容。 ,因此(在您的情况下)您可能需要将所有图层移动到没有其他内容的 subview 中,因此可以安全地屏蔽。

更新

看看你的操场,问题是(仍然)你正在绘制两个形状,它们彼此顶部具有完全相同的部分透明边缘。你不能那样做。解决方法是将彩色形状画得更大,使它们在 donut 边缘完全不透明,然后使用图层蒙版将它们剪辑到 donut 形状。

我修好了你的操场。注意在我的版本中,lineWidth每个彩色部分的 donutThickness + 10 ,以及面具的lineWidthdonutThickness .结果如下:

playground output

这里是游乐场:
import UIKit
import PlaygroundSupport

class ABDonutChart: UIView {

    struct Datum {
        var value: Double
        var color: UIColor
    }

    var donutThickness: CGFloat = 20 { didSet { setNeedsLayout() } }
    var separatorValue: Double = 1 { didSet { setNeedsLayout() } }
    var separatorColor: UIColor = .white { didSet { setNeedsLayout() } }
    var data = [Datum]() { didSet { setNeedsLayout() } }

    func withAnimation(_ wantAnimation: Bool, do body: () -> ()) {
        let priorFlag = wantAnimation
        self.wantAnimation = true
        defer { self.wantAnimation = priorFlag }
        body()
        layoutIfNeeded()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        let bounds = self.bounds
        let center = CGPoint(x: bounds.origin.x + bounds.size.width / 2, y: bounds.origin.y + bounds.size.height / 2)
        let radius = (min(bounds.size.width, bounds.size.height) - donutThickness) / 2

        let maskLayer = layer.mask as? CAShapeLayer ?? CAShapeLayer()
        maskLayer.frame = bounds
        maskLayer.fillColor = nil
        maskLayer.strokeColor = UIColor.white.cgColor
        maskLayer.lineWidth = donutThickness
        maskLayer.path = CGPath(ellipseIn: CGRect(x: center.x - radius, y: center.y - radius, width: 2 * radius, height: 2 * radius), transform: nil)
        layer.mask = maskLayer

        var spareLayers = segmentLayers
        segmentLayers.removeAll()

        let finalSum = data.reduce(Double(0)) { $0 + $1.value + separatorValue }
        var runningSum: Double = 0

        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.fromValue = 0.0
        animation.toValue = 1.0
        animation.duration = 2
        animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

        func addSegmentLayer(color: UIColor, segmentSum: Double) {
            let angleOffset: CGFloat = -0.25 * 2 * .pi

            let segmentLayer = spareLayers.popLast() ?? CAShapeLayer()
            segmentLayer.strokeColor = color.cgColor
            segmentLayer.lineWidth = donutThickness + 10
            segmentLayer.lineCap = kCALineCapButt
            segmentLayer.fillColor = nil

            let path = CGMutablePath()
            path.addArc(center: center, radius: radius, startAngle: angleOffset, endAngle: CGFloat(segmentSum / finalSum * 2 * .pi) + angleOffset, clockwise: false)
            segmentLayer.path = path

            layer.insertSublayer(segmentLayer, at: 0)
            segmentLayers.append(segmentLayer)

            if wantAnimation {
                segmentLayer.add(animation, forKey: animation.keyPath)
            }
        }

        for datum in data {
            addSegmentLayer(color: separatorColor, segmentSum: runningSum + separatorValue / 2)
            runningSum += datum.value + separatorValue
            addSegmentLayer(color: datum.color, segmentSum: runningSum - separatorValue / 2)
        }

        addSegmentLayer(color: separatorColor, segmentSum: finalSum)

        spareLayers.forEach { $0.removeFromSuperlayer() }
    }

    private var segmentLayers = [CAShapeLayer]()
    private var wantAnimation = false
}

let container = UIView()
container.frame.size = CGSize(width: 300, height: 300)
container.backgroundColor = .black
PlaygroundPage.current.liveView = container
PlaygroundPage.current.needsIndefiniteExecution = true

let m = ABDonutChart(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)
container.addSubview(m)

m.withAnimation(true) {
    m.data = [
        .init(value: 10, color: .red),
        .init(value: 30, color: .blue),
        .init(value: 15, color: .orange),
        .init(value: 40, color: .yellow),
        .init(value: 50, color: .green)]
}

关于ios - 带有 CAShapelayer 的圆弧圆环图 - 底层的边界可见,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43633059/

相关文章:

ios - Xcode 预处理器宏检查 Base SDK 是否 >= iOS 7.0

ios - 带有圆角和投影的 UICollectionViewCell 不起作用

ios - 如何在iOS中绘制类似图形的平行四边形?

ios - 如何在IOS中制作月亮绕地球旋转同时自己旋转的CAAnimation效果?

ios - 如何绘制不同颜色的填充路径/形状

ios - iOS绘图圈百分比状态的路径

iPhone 服务器客户端应用程序

iphone - iOS 原始视频捕获 - 从哪里开始?

ios - 魔法记录,无法保存对象 : contextDidSave == NO, error = nil

ios - 在哪里调用 setNeedsDisplay 来更新 CALayer?