【问题标题】:arcs donut chart with CAShapelayer - border of underlaying layers are visible带有 CAShapelayer 的圆弧圆环图 - 底层的边界可见
【发布时间】:2017-04-26 11:35:30
【问题描述】:

我用 CAShapeLayers 弧线绘制了一个圆环图。我通过将一个放在另一个上面来绘制它,并且下面的层边缘是可见的问题。

绘图代码如下

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)

我做错了什么?

更新

尝试过的代码

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

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

【问题讨论】:

    标签: ios calayer cashapelayer


    【解决方案1】:

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

    我们必须记住,我们关于 几何对象对子像素区域的划分中断 面对具有相关遮罩的输入图片。 当一张图片在合成表达式中出现两次时, 我们必须小心计算 F A 和 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)
    }
    

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

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

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

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

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

    这是绘制完美版本的代码:

    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。结果如下:

    这里是游乐场:

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

    【讨论】:

    • 这是一个最好的答案,非常感谢!一个问题 - 为了避免在戒指外出现边缘,我也需要有外罩(就像你用黑色画的一样)?
    • 黑色只是顶层视图的backgroundColor
    • 在第二张图片中,您有外边缘,而在第三张图片中 - 没有。要做到这一点,我必须使用外层掩码?
    • 一共有四张图片,前三张都是边缘的。只有一个蒙版,在第四张图中只作为蒙版使用。
    • 我试过你的代码,但它只掩盖了内部边缘,外部仍然有边缘。你能看一下upd吗?
    【解决方案2】:

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

    layer.Opaque = true; //C#
    

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

    【讨论】:

    • 不透明没有帮助。圆圈也无法绘制,因为背景中有图像
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-30
    • 1970-01-01
    • 1970-01-01
    • 2011-10-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多