如何为圆圈制作透明轮廓以便背景显示出来?

问题描述 投票:0回答:2

如何为圆圈制作透明轮廓以使背景显示出来?像这样:

enter image description here

我尝试使用掩码,但此代码不起作用......

也许还有其他解决方案?

我的代码:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let backgroundImage = UIImageView(frame: UIScreen.main.bounds)
        backgroundImage.image = UIImage(named: "background")
        backgroundImage.contentMode = .scaleAspectFill
        view.addSubview(backgroundImage)
        view.sendSubviewToBack(backgroundImage)

        // Параметры для кругов
        let circleSize: CGFloat = 100 
        let borderWidth: CGFloat = 4 
        let circleSpacing: CGFloat = 30 

        var previousCircleView: UIImageView?

        for i in 0..<3 {
            let circleImageView = UIImageView()
            circleImageView.image = UIImage(named: "ggg")
            circleImageView.contentMode = .scaleAspectFill
            view.addSubview(circleImageView)

            let maskLayer = CAShapeLayer()
            let circlePath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: circleSize, height: circleSize))
            let borderPath = UIBezierPath(ovalIn: CGRect(x: -borderWidth, y: -borderWidth, width: circleSize + 2 * borderWidth, height: circleSize + 2 * borderWidth))
            borderPath.append(circlePath)
            borderPath.usesEvenOddFillRule = true

            maskLayer.path = borderPath.cgPath
            maskLayer.fillRule = .evenOdd
            circleImageView.layer.mask = maskLayer

            circleImageView.snp.makeConstraints { make in
                make.width.height.equalTo(circleSize + 2 * borderWidth)
                make.centerY.equalToSuperview()

                if let previous = previousCircleView {
                    make.left.equalTo(previous.snp.right).offset(-(circleSize - circleSpacing + borderWidth))
                } else {
                    make.left.equalToSuperview().offset(20)
                }
            }

            previousCircleView = circleImageView
        }
    }
}
ios swift uikit layer snapkit
2个回答
1
投票

每个形状都由两条弧线组成。唯一的技巧是计算每个角度的起始角和结束角,需要一点三角学知识。例如,您可以执行以下操作:

func addSublayers(to view: UIView) {
    let radius: CGFloat = 150
    let borderWidth: CGFloat = 10
    let circleCenterOffset: CGFloat = 120
    let startCenter = CGPoint(x: 200, y: 200)

    for i in 0..<3 {
        let shapeLayer = createShapeLayer()
        shapeLayer.path = circleDifference(
            center: point(startCenter, xOffset: CGFloat(i) * circleCenterOffset),
            radius: radius,
            offset: circleCenterOffset,
            width: borderWidth
        ).cgPath
        view.layer.addSublayer(shapeLayer)
    }

    let shapeLayer = createShapeLayer()
    shapeLayer.path = UIBezierPath(arcCenter: point(startCenter, xOffset: 3 * circleCenterOffset), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
    view.layer.addSublayer(shapeLayer)
}

private func point(_ point: CGPoint, xOffset: CGFloat) -> CGPoint {
    CGPoint(x: point.x + xOffset, y: point.y)
}

private func createShapeLayer() -> CAShapeLayer {
    let shapeLayer = CAShapeLayer()
    shapeLayer.strokeColor = UIColor.clear.cgColor
    shapeLayer.fillColor = …
    return shapeLayer
}

private func circleDifference(center: CGPoint, radius: CGFloat, offset: CGFloat, width: CGFloat) -> UIBezierPath {
    let r1 = radius
    let r2 = radius + width
    let d = offset

    // angles; using law of cosines
    let angle1 = acos((r1 * r1 + d * d - r2 * r2) / (2 * r1 * d))
    let angle2 = acos((r2 * r2 + d * d - r1 * r1) / (2 * r2 * d))

    // path
    let path = UIBezierPath()
    path.addArc(
        withCenter: center,
        radius: r1,
        startAngle: angle1,
        endAngle: 2 * .pi - angle1,
        clockwise: true
    )
    path.addArc(
        withCenter: point(center, xOffset: offset),
        radius: r2,
        startAngle: 3 * .pi / 2 - (.pi / 2 - angle2),
        endAngle: .pi / 2 + (.pi / 2 - angle2),
        clockwise: false
    )
    path.close()
    return path
}

结果:


如果您想了解这里的三角学,请考虑下图,其中左侧红色圆圈是要渲染的圆圈(减去右侧外部绿色圆圈(即下一个圆圈)、内部右侧蓝色圆圈,加上任何间距我们要)。因此,我们要绘制红色和绿色圆圈的弧,以与这些圆圈相交的位置相对应的角度开始和停止弧。

所以,我想象了一个三角形(蓝色阴影),其中顶点是两个圆的中心加上两个圆相交的位置。抚摸两条圆弧时使用的角度是该三角形的左下角和右下角。我们知道这个三角形所有边的长度(左边是圆的半径,右边是圆的半径加上间距,底线是圆之间的距离)。

在我原来的答案中,我计算了三角形的高度,h,短垂直紫色线段(使用划分面积的技术,用Heron公式计算,除以半周长),然后计算两者使用

asin(h/r)
.

的角度

但是我修改了我的示例,通过余弦定律计算两个角度。这对大多数读者来说可能会更直观。


如果所有这些数学太令人困惑,您可以将每个形状图层添加为被另一个圆圈遮盖的圆圈:

func addSublayers(to view: UIView) {
    let radius: CGFloat = 150
    let borderWidth: CGFloat = 10
    let circleCenterOffset: CGFloat = 120
    let startCenter = CGPoint(x: 200, y: 200)

    for i in 0..<3 {
        let shapeLayer = createShapeLayer()
        var center = point(startCenter, xOffset: CGFloat(i) * circleCenterOffset)
        shapeLayer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath

        let mask = CAShapeLayer()
        mask.fillColor = UIColor.white.cgColor
        mask.strokeColor = UIColor.clear.cgColor

        center = point(startCenter, xOffset: CGFloat(i + 1) * circleCenterOffset)
        let path = UIBezierPath(rect: view.bounds)
        path.addArc(withCenter: center, radius: radius + borderWidth, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
        mask.fillRule = .evenOdd
        mask.path = path.cgPath

        shapeLayer.mask = mask

        view.layer.addSublayer(shapeLayer)
    }

    let shapeLayer = createShapeLayer()
    let center = point(startCenter, xOffset: 3 * circleCenterOffset)
    shapeLayer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
    view.layer.addSublayer(shapeLayer)
}

就我个人而言,我总是倾向于在没有任何掩码的情况下创建所需的路径,但后一种方法完全避免了任何令人困惑的数学。


0
投票

如果您的目标是一系列以整圆结尾的新月,则创建形状图层和蒙版将涉及绘制部分圆弧和一些三角函数。

您可以通过创建包含所需内容的图像视图来实现此目的。在这种情况下,您可以编写在循环中运行的代码,用不透明的颜色绘制一个圆圈,然后切换到清晰模式并擦除圆圈的轮廓。每个新圆都会擦除前一个圆的一部分,但最后一个圆不会被剪掉。这就是您的示例图像所显示的内容。

这是我编写的用于创建该图像的代码,大致基于您的起始代码:

import UIKit

class ViewController: UIViewController {
    
    var imageView = UIImageView(frame: CGRectZero)
    
    
    // Install an image in imageView that contains a series of crescent shapes, ending in a full circle.
    private func setShades(count: Int) {
        var image = UIImage()
        let lineWidth = 7.0
        let circleDiameter = 50.0
        let width = CGFloat(count+1)/2.0 * circleDiameter
        let bounds = CGRect(origin: CGPointZero, size: CGSize(width: width, height: circleDiameter))
        let circleColor =  UIColor(_colorLiteralRed: 161.0/255, green: 62.0/255, blue: 3.0/255, alpha: 1.0)
        let renderer = UIGraphicsImageRenderer(size: bounds.size)
        image = renderer.image { context in
            for index in 0..<count {
                let x = CGFloat(index) * circleDiameter / 2
                let circleRect = CGRect(
                    x: x, y: 0,
                    width: circleDiameter, height: circleDiameter)
                let circle = UIBezierPath.init(ovalIn: circleRect)
                circle.lineWidth = lineWidth
                circleColor.setFill()
                context.cgContext.setBlendMode(.normal)
                
                //Fill the circle with our circle color
                circle.fill()
                
                //Switch the drawing mode to .clear, so we erase anything we draw
                context.cgContext.setBlendMode(.clear)
                
                // Erase the outline of this circle
                circle.stroke()
            }
        }
        // Place the image view near the bottom of the content view, and on the right side.
        let frame = CGRect(x: view.bounds.maxX - bounds.width, y: view.bounds.maxY - 100 - bounds.height, width: bounds.width, height: bounds.height)
        imageView.frame = frame
        imageView.image = image
    }

    override func viewDidLayoutSubviews() {
        // You need to create teh image view when the view changes its subviews
        //so that it is placed correctly (e.g. after device rotation)
        setShades(count: 4)
    }

    override func viewDidLoad() {
        super.viewDidLoad()


        let backgroundImage = UIImageView(frame: UIScreen.main.bounds)
        backgroundImage.image = UIImage(named: "background")
        backgroundImage.contentMode = .scaleAspectFill
        view.addSubview(backgroundImage)
        view.addSubview(imageView)
    }
}

这里是 Github 上的一个 示例项目,它创建了下面的图像:

© www.soinside.com 2019 - 2024. All rights reserved.