带有透明孔的CALayer

121

我有一个简单的视图(图片左侧),需要为此视图创建某种覆盖层(图片右侧)。这个覆盖层应该有一定的透明度,以便下面的视图仍然部分可见。最重要的是,这个覆盖层应该在中间有一个圆形孔,这样它就不会覆盖视图的中心(请参见下图)。

我可以很容易地创建这样的圆形:

int radius = 20; //whatever
CAShapeLayer *circle = [CAShapeLayer layer];

circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0,radius,radius) cornerRadius:radius].CGPath;
circle.position = CGPointMake(CGRectGetMidX(view.frame)-radius,
                              CGRectGetMidY(view.frame)-radius);
circle.fillColor = [UIColor clearColor].CGColor;

还有一个像这样的“全”矩形覆盖层:

CAShapeLayer *shadow = [CAShapeLayer layer];
shadow.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, view.bounds.size.width, view.bounds.size.height) cornerRadius:0].CGPath;
shadow.position = CGPointMake(0, 0);
shadow.fillColor = [UIColor grayColor].CGColor;
shadow.lineWidth = 0;
shadow.opacity = 0.5;
[view.layer addSublayer:shadow];

但是我不知道如何将这两个图层组合起来,以达到我想要的效果。有人知道吗?我已经尝试了很多方法...非常感谢你的帮助!

Image


你能否创建一个包含矩形和圆的贝塞尔曲线,然后在绘制时使用奇偶规则来创建一个孔(我还没有尝试过)? - Wain
我不知道怎么做 :) - animal_chin
使用rect创建路径,然后使用moveToPoint方法移动到指定点,最后添加圆角矩形。请查看UIBezierPath提供的方法文档。 - Wain
请查看以下类似的问题和答案是否有所帮助:在UIView中裁剪透明孔 - dichen
请查看我的解决方案:https://dev59.com/M2zXa4cB1Zd3GeqPXMP-#32941652 希望这能帮助到某些人。 - James Laurenstin
5个回答

229

我能够通过Jon Steinmetz的建议解决这个问题。如果有人在意,这是最终解决方案:

int radius = myRect.size.width;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.mapView.bounds.size.width, self.mapView.bounds.size.height) cornerRadius:0];
UIBezierPath *circlePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius];
[path appendPath:circlePath];
[path setUsesEvenOddFillRule:YES];

CAShapeLayer *fillLayer = [CAShapeLayer layer];
fillLayer.path = path.CGPath;
fillLayer.fillRule = kCAFillRuleEvenOdd;
fillLayer.fillColor = [UIColor grayColor].CGColor;
fillLayer.opacity = 0.5;
[view.layer addSublayer:fillLayer];

Swift 3.x:

let radius = myRect.size.width
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.mapView.bounds.size.width, height: self.mapView.bounds.size.height), cornerRadius: 0)
let circlePath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 2 * radius, height: 2 * radius), cornerRadius: radius)
path.append(circlePath)
path.usesEvenOddFillRule = true

let fillLayer = CAShapeLayer()
fillLayer.path = path.cgPath
fillLayer.fillRule = kCAFillRuleEvenOdd
fillLayer.fillColor = Color.background.cgColor
fillLayer.opacity = 0.5
view.layer.addSublayer(fillLayer)

Swift 4.2 & 5:

let radius: CGFloat = myRect.size.width
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height), cornerRadius: 0)
let circlePath = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 2 * radius, height: 2 * radius), cornerRadius: radius)
path.append(circlePath)
path.usesEvenOddFillRule = true

let fillLayer = CAShapeLayer()
fillLayer.path = path.cgPath
fillLayer.fillRule = .evenOdd
fillLayer.fillColor = view.backgroundColor?.cgColor
fillLayer.opacity = 0.5
view.layer.addSublayer(fillLayer)

2
为了增加灵活性,可以将您的视图子类化为“IBDesignable”。这非常容易!要开始,请将上面的代码插入到我在此问题中提供的答案中:https://dev59.com/M2zXa4cB1Zd3GeqPXMP-#31255084 - clozach
2
作为一名初学者iOS开发者,我花了几个小时来弄清楚为什么这段代码会产生奇怪的结果。最终我发现,如果叠加层在某个时刻重新计算,则必须删除添加的子层。这可以通过view.layer.sublayers属性实现。非常感谢您的回答! - Serzhas
我为什么会得到它的完全相反结果?清晰的有颜色的图层上面还有一个半透明的黑色形状? - Chanchal Warde
我该如何使用这种模式将透明文本添加到圆形中,是否可能?我找不到方法。 - Diego Fernando Murillo Valenci
有人能够提供一些关于如何实现这个的文档吗?如果我为图层路径添加三个或更多的排除项,我会得到奇怪的(连接)路径。 - Amber K
显示剩余3条评论

34

为了实现这个效果,我发现最容易的方法是创建一个完整的视图覆盖屏幕,然后使用图层和UIBezierPaths减去屏幕的部分。对于Swift实现:

// Create a view filling the screen.
let overlay = UIView(frame: CGRectMake(0, 0, 
    UIScreen.mainScreen().bounds.width,
    UIScreen.mainScreen().bounds.height))

// Set a semi-transparent, black background.
overlay.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.85)

// Create the initial layer from the view bounds.
let maskLayer = CAShapeLayer()
maskLayer.frame = overlay.bounds
maskLayer.fillColor = UIColor.blackColor().CGColor

// Create the frame for the circle.
let radius: CGFloat = 50.0
let rect = CGRectMake(
        CGRectGetMidX(overlay.frame) - radius,
        CGRectGetMidY(overlay.frame) - radius,
        2 * radius,
        2 * radius)

// Create the path.
let path = UIBezierPath(rect: overlay.bounds)
maskLayer.fillRule = kCAFillRuleEvenOdd

// Append the circle to the path so that it is subtracted.
path.appendPath(UIBezierPath(ovalInRect: rect))
maskLayer.path = path.CGPath

// Set the mask of the view.
overlay.layer.mask = maskLayer

// Add the view so it is visible.
self.view.addSubview(overlay)

我测试了上面的代码,这是结果:

enter image description here

我添加了一个库到 CocoaPods 中,它抽象出了很多上面的代码,并允许你轻松创建带有矩形/圆形孔的覆盖视图,使用户可以与覆盖视图后面的视图进行交互。我使用它为我们的一个应用程序创建了这个教程:

Tutorial using the TAOverlayView

这个库叫做TAOverlayView,是在 Apache 2.0 下的开源项目。希望你会发现它有用!


此外,请勿发布重复答案。相反,考虑其他可以帮助未来用户找到所需答案的操作,如链接帖子中所述。当这些答案几乎只是一个链接和推荐使用您的东西时,它们看起来非常像垃圾邮件。 - Mogsdad
1
@Mogsdad 我并不想显得像是在垃圾邮件,我只是花了很多时间在这个库上,我觉得对于那些正在尝试做类似事情的人来说会很有用。但还是谢谢你的反馈,我会更新我的回答,使用代码示例。 - Nick Yap
3
很好的更新,Nick。我支持你 - 我自己也发布了一些库和工具,我明白在我的文档已经包含答案时,提供完整答案似乎有些冗余...但是这个想法是为了让答案尽可能地自包含。而且确实有一些人只发布垃圾信息,所以我不想被他们混淆。我认为你也是这么想的, 所以我向你指出了这一点。干杯! - Mogsdad
我使用了您创建的pod,非常感谢。但是我的叠加层下面的视图停止交互了。这是怎么回事?我有一个包含图像视图的滚动视图。 - Ammar Mujeeb
@AmmarMujeeb 叠加层会阻止除你创建的“孔”之外的交互。我设计这个 pod 的初衷是为了创建突出屏幕部分的叠加层,并且只允许与突出显示的元素进行交互。 - Nick Yap
显示剩余2条评论

11

已接受的解决方案 Swift 3.0 兼容

let radius = myRect.size.width
let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: self.mapView.bounds.size.width, height: self.mapView.bounds.size.height), cornerRadius: 0)
let circlePath = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 2.0*radius, height: 2.0*radius), cornerRadius: radius)
path.append(circlePath)
path.usesEvenOddFillRule = true

let fillLayer = CAShapeLayer()
fillLayer.path = path.cgPath
fillLayer.fillRule = kCAFillRuleEvenOdd
fillLayer.fillColor = UIColor.gray.cgColor
fillLayer.opacity = 0.5
view.layer.addSublayer(fillLayer)

@Fattie:你的链接已经失效了。 - Randy

10

我采用了与animal_chin类似的方法,但更注重视觉效果,因此我在Interface Builder中使用了outlets和自动布局来设置大部分内容。

这是我的Swift解决方案

    //shadowView is a UIView of what I want to be "solid"
    var outerPath = UIBezierPath(rect: shadowView.frame)

    //croppingView is a subview of shadowView that is laid out in interface builder using auto layout
    //croppingView is hidden.
    var circlePath = UIBezierPath(ovalInRect: croppingView.frame)
    outerPath.usesEvenOddFillRule = true
    outerPath.appendPath(circlePath)

    var maskLayer = CAShapeLayer()
    maskLayer.path = outerPath.CGPath
    maskLayer.fillRule = kCAFillRuleEvenOdd
    maskLayer.fillColor = UIColor.whiteColor().CGColor

    shadowView.layer.mask = maskLayer

我喜欢这个解决方案,因为你可以在设计时和运行时非常容易地移动circlePath。 - Mark Moeykens
虽然我将其修改为使用普通矩形而不是椭圆形,但这对我没有起作用,最终的遮罩图像仍然出错 :( - GameDev

7

兼容Swift 2.0的代码

从@animal_inch的答案开始,我编写了一个小型实用程序类,希望它能得到赞赏:

import Foundation
import UIKit
import CoreGraphics

/// Apply a circle mask on a target view. You can customize radius, color and opacity of the mask.
class CircleMaskView {

    private var fillLayer = CAShapeLayer()
    var target: UIView?

    var fillColor: UIColor = UIColor.grayColor() {
        didSet {
            self.fillLayer.fillColor = self.fillColor.CGColor
        }
    }

    var radius: CGFloat? {
        didSet {
            self.draw()
        }
    }

    var opacity: Float = 0.5 {
        didSet {
           self.fillLayer.opacity = self.opacity
        }
    }

    /**
    Constructor

    - parameter drawIn: target view

    - returns: object instance
    */
    init(drawIn: UIView) {
        self.target = drawIn
    }

    /**
    Draw a circle mask on target view
    */
    func draw() {
        guard (let target = target) else {
            print("target is nil")
            return
        }

        var rad: CGFloat = 0
        let size = target.frame.size
        if let r = self.radius {
            rad = r
        } else {
            rad = min(size.height, size.width)
        }

        let path = UIBezierPath(roundedRect: CGRectMake(0, 0, size.width, size.height), cornerRadius: 0.0)
        let circlePath = UIBezierPath(roundedRect: CGRectMake(size.width / 2.0 - rad / 2.0, 0, rad, rad), cornerRadius: rad)
        path.appendPath(circlePath)
        path.usesEvenOddFillRule = true

        fillLayer.path = path.CGPath
        fillLayer.fillRule = kCAFillRuleEvenOdd
        fillLayer.fillColor = self.fillColor.CGColor
        fillLayer.opacity = self.opacity
        self.target.layer.addSublayer(fillLayer)
    }

    /**
    Remove circle mask
    */


  func remove() {
        self.fillLayer.removeFromSuperlayer()
    }

}

然后,在您的代码中的任何位置:

let circle = CircleMaskView(drawIn: <target_view>)
circle.opacity = 0.7
circle.draw()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接