带有CAShapelayer的圆弧圆环图 - 可以看到底层图层的边框

时间:2017-04-26 11:35:30

标签: 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

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

2 个答案:

答案 0 :(得分:4)

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

  

我们必须记住我们关于的基本假设   通过几何对象划分子像素区域   面对具有相关遮罩的输入图片。   当一张图片在合成表达式中出现两次时,   我们必须注意F A和F的计算   F B.表中列出的内容仅适用于不相关的内容   图片。

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

这是一个独立的测试,可以重现您的问题:

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

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

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中的所有,因此(在您的情况下)您可能需要将所有图层移动到子视图中而没有其他内容,因此它是安全的掩模。

更新

看着你的游乐场,问题是(仍然)你正在绘制两个形状,它们在彼此的顶部具有完全相同的部分透明边缘。你不能这样做。解决方案是绘制更大的彩色形状,使它们在甜甜圈的边缘都是完全不透明的,然后使用图层蒙版将它们剪裁成圆环形状。

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

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

答案 1 :(得分:0)

对我来说,看起来边缘是抗锯齿的,导致像素有点透明。然后可以通过叠加层的“模糊”边缘看到背景的橙色。 您是否尝试过使覆盖层不透明?

layer.Opaque = true; //C#

另一种方法可能是在橙色边缘上绘制一个背景颜色的细圆。这应该有用,但这不是最漂亮的方法。