带有透明孔的CALayer


112

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

我可以轻松地创建一个这样的圆:

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];

但是我不知道如何将这两层结合起来,以便它们产生我想要的效果。任何人?我真的已经尝试了一切...非常感谢您的帮助!

图片


您能否创建一个包含rect和圆的贝塞尔曲线,然后在绘制过程中使用的绕线规则将创建一个孔(我没有尝试过)。
北斗星

我不知道该怎么做:)
animal_chin

使用rect创建,然后使用moveToPoint,然后添加四舍五入的rect。检查文档中提供的方法UIBezierPath
北斗星

看看这个类似的问题和答案的帮助:在UIView的切割透明窟窿] [1] [1]:stackoverflow.com/questions/9711248/...
迪辰

在这里查看我的解决方案:stackoverflow.com/questions/14141081/… 希望这
会对

Answers:


218

我能够通过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”。真的很简单!首先,将上面的代码插入我对这个问题给出的答案:stackoverflow.com/questions/14141081/…–
clozach

2
作为iOS新手开发人员,我花了几个小时来弄清楚为什么这段代码会产生奇怪的结果。最终我发现,如果在某个时候重新计算了叠加蒙版,则必须删除添加的子层。这可以通过view.layer.sublayers属性来实现。非常感谢您的回答!
塞尔扎斯

为什么我得到的恰恰相反。黑色半透明形状的透明着色层?
Chanchal Warde'7

如何使用此模式将透明文本添加到圆中?我不知道如何
迭戈·费尔南多·穆里略·瓦伦奇

将近6年仍会有所帮助,但请记住,空心孔并不能真正“刺穿”容纳该孔的层。说,如果将孔覆盖在按钮上。该按钮不可访问,如果您要制作像我这样的“指导教程”,则需要此按钮。@Nick Yap提供的库将通过覆盖func点(内部点:CGPoint,事件:UIEvent?)-> UIView的Bool {}来为您完成此工作。查看他的图书馆以获取更多详细信息。但是,您期望的只是“对幕后的可见性”,这是一个有效的答案。
infinity_coding7年

32

为了创建这种效果,我发现最简单的方法是创建覆盖屏幕的整个视图,然后使用图层和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)

我测试了上面的代码,结果如下:

在此处输入图片说明

我在CocoaPods中添加了一个库,该库抽象了许多上述代码,并允许您轻松创建带有矩形/圆形孔的叠加层,从而允许用户与叠加层后面的视图进行交互。我用它为我们的一个应用程序创建了本教程:

使用TAOverlayView的教程

该库称为TAOverlayView,在Apache 2.0下是开源的。希望对你有帮助!


另外,请勿发布重复的答案。相反,请考虑其他可帮助将来用户找到所需答案的操作,如链接文章中所述。当这些答案仅是使用您的内容的链接和建议时,它们看起来就很垃圾。
Mogsdad '16

1
@Mogsdad我不想显得垃圾邮件,我只是在这个库上花了很多时间,我认为这对于尝试做类似事情的人很有用。但是,感谢您的反馈,我将更新我的答案以使用代码示例
Nick Yap

3
很好,尼克。我在您的角落里-我本人已经发布了库和实用程序,并且我了解,当我的文档已经涵盖完整的答案时,将完整的答案放在此处似乎是多余的...但是,其想法是将答案保持为独立的尽可能。还有人发帖无非是垃圾邮件,所以我宁愿不与他们混为一谈。我认为您是同一个人,这就是为什么我向您指出这一点。干杯!
Mogsdad '16

我使用了您创建的豆荚,谢谢。但是叠加层下面的视图停止了交互。它出什么问题了?我有一个带有imageview的Scrollview。
Ammar Mujeeb

@AmmarMujeeb覆盖层会阻止交互,除非通过您创建的“孔”。我使用广告连播的目的是覆盖突出显示屏幕的部分,并且仅允许您与突出显示的元素进行交互。
尼克·叶(Yap Yap)

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中进行了大多数设置。

这是我在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。
马克·莫肯斯

这对我不起作用,尽管我将其修改为使用普通的rect而不是椭圆形,但是最终的蒙版图像刚刚出现错误:(
GameDev

7

兼容Code 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()
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.