core graphics绘图时如何跳过一个区域(cutout)

How to skip an area (cutout) when drawing with core graphics

我想绘制一些项目,但为圆形区域留下真正的 alpha 透明度切口。我想达到的目标:

黄色是显示渗色的示例背景。

切口宽度实际上比弧线更宽,所以它们没有完全相交。我需要真正的剪切图,因为我要保存为具有透明度的图像。

我想也许我可以使用 setBlendMode(),但我相信只有当我希望我的切口与弧的宽度完全相同时,它才会起作用。但是我试图解决它的要点是:

Swift 工作簿如下。非常感谢有关实现此目的的任何提示。

import Foundation
import UIKit

var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;

class DonutView : UIView
{
    override func draw(_ rect: CGRect)
    {

        // cutout
        let cutoutColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
        cutoutColor.setFill()
        let cutoutPath = UIBezierPath(ovalIn: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
        cutoutPath.fill()

//        let context = UIGraphicsGetCurrentContext()!
//        context.setBlendMode(.sourceOut)

        let ringOffset = cutoutWidth/2;
        let circleWidth = dimen - ringOffset*2;

        // ring
        let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
        let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
        ringColor.setStroke()
        ringPath.lineWidth = strokeWidth
        ringPath.stroke()

        // arc
        let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
        let arcPath = UIBezierPath()
        arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)

        let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
        arcColor.setStroke()
        arcPath.lineWidth = strokeWidth
        arcPath.stroke()
    }
}

var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow

// View these elements
view

(编辑:我应该在一开始就说明这一点:这是最终为 WatchKit 创建一个 UIImage)

的帮助下
import Foundation
import UIKit
import PlaygroundSupport

var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;

class DonutView : UIImageView
{
    override init(frame: CGRect) {
        super.init(frame: frame)

        UIGraphicsBeginImageContextWithOptions(CGSize(width: dimen, height: dimen), false, 1)
        let context = UIGraphicsGetCurrentContext()!

        let ringOffset = cutoutWidth/2;
        let circleWidth = dimen - ringOffset*2;

        // ring
        let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
        let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
        ringColor.setStroke()
        ringPath.lineWidth = strokeWidth
        ringPath.stroke()

        // arc
        let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
        let arcPath = UIBezierPath()
        arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)        
        let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
        arcColor.setStroke()
        arcPath.lineWidth = strokeWidth
        arcPath.stroke()

        // Cutout circle
        context.setFillColor(UIColor.clear.cgColor)
        context.setBlendMode(.clear)        
        context.addEllipse(in: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
        context.drawPath(using: .fill)
        context.setBlendMode(.normal)

        image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()        
        }

    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow

// View these elements
view

您可以使用另一个 CAShapeLayer 作为掩码来完成此操作。

alpha = 1.0 的遮罩层部分将完全透明。

所以...

如果我们将 Arc Layer 设为 Ring Layer 的子层,然后我们可以将 Cutout Layer 用作蒙版,结果:

这是 Playground 页面的来源:

class MyDonutView : UIView
{

    let ringLayer = CAShapeLayer()
    let arcLayer = CAShapeLayer()
    let cutoutLayer = CAShapeLayer()

    var strokeWidth: CGFloat = 20.0;
    var cutoutWidth: CGFloat = 30.0;

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    func commonInit() -> Void {

        // add arcLayer as a sublayer of ringLayer
        ringLayer.addSublayer(arcLayer)

        // add ringLayer as a sublayer of self.layer
        layer.addSublayer(ringLayer)

        // ring layer stroke is black at 0.3 alpha, fill is clear
        ringLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.3).cgColor
        ringLayer.fillColor = UIColor.clear.cgColor
        ringLayer.lineWidth = strokeWidth

        // arc layer stroke is black at 0.6 alpha, fill is clear
        arcLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.6).cgColor
        arcLayer.lineWidth = strokeWidth
        arcLayer.fillColor = UIColor.clear.cgColor

        // cutout layer stroke is black (although we're using Zero line width
        //  fill is black
        cutoutLayer.strokeColor = UIColor.red.cgColor
        cutoutLayer.lineWidth = 0
        cutoutLayer.fillColor = UIColor.red.cgColor

    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // define the "padding" around the ring
        let ringOffset = cutoutWidth / 2.0

        // define the diameter of the ring
        let circleWidth = bounds.size.width - cutoutWidth;

        // ring path
        let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))

        // arc path
        let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
        let arcPath = UIBezierPath()
        arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)

        // set ring layer path
        ringLayer.path = ringPath.cgPath

        // set arc layer path
        arcLayer.path = arcPath.cgPath

        // create a rect path the full size of bounds of self
        let fullPath = UIBezierPath(rect: bounds)

        // create a cutout path (the small circle to cut-out of the ring/arc)
        let cutoutPath = UIBezierPath(ovalIn: CGRect(x: bounds.size.width-cutoutWidth, y: bounds.size.width/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))

        // append the cutout path to the full rect path
        fullPath.append(cutoutPath)

        // even-odd winding rule
        cutoutLayer.fillRule = CAShapeLayerFillRule.evenOdd

        // set cutout layer path
        cutoutLayer.path = fullPath.cgPath

        // use cutout layer to mask ring layer
        ringLayer.mask = cutoutLayer
    }

}

class TestViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white

        // instantiate a MyDonutView
        let myDonutView = MyDonutView()

        // we can set the stroke and cutout widths here
        myDonutView.strokeWidth = 20.0
        myDonutView.cutoutWidth = 30.0

        // we're using auto-layout
        myDonutView.translatesAutoresizingMaskIntoConstraints = false

        // background color yellow to see the frame
        //myDonutView.backgroundColor = .yellow

        // otherwise, it should be clear
        myDonutView.backgroundColor = .clear

        // add as subview
        view.addSubview(myDonutView)

        // constrain centerX and centerY
        // width = 200, height = width
        NSLayoutConstraint.activate([

            myDonutView.widthAnchor.constraint(equalToConstant: 200.0),
            myDonutView.heightAnchor.constraint(equalTo: myDonutView.widthAnchor),
            myDonutView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            myDonutView.centerYAnchor.constraint(equalTo: view.centerYAnchor),

            ])

    }

}

let vc = TestViewController()
PlaygroundPage.current.liveView = vc