如何在面具周围绘制CALayer边框?

时间:2022-04-05 23:43:20

So, I have a CALayer, which has a mask & I want to add border around this layer's mask. For example, I have set triangle mask to the layer and I want to have border around that layer.

所以,我有一个CALayer,它有一个面具,我想在这个图层的蒙版周围添加边框。例如,我已经为图层设置了三角形蒙版,我希望在该图层周围设置边框。

Can anyone please help me to solve this problem?

有人可以帮我解决这个问题吗?

6 个解决方案

#1


4  

Some suggestions:

  • Use an opaque shadow instead of a border (you will have a blurred effect).
  • 使用不透明阴影而不是边框​​(您将产生模糊效果)。

  • Create another layer, set its background color with the color you want for your border, mask it with a mask slightly bigger than the one you already have to simulate the border width, and put it centered behind your layer (may not work with every shape).
  • 创建另一个图层,使用您想要的边框颜色设置其背景颜色,使用比模拟边框宽度稍大的面具遮住它,并将其置于图层后面(可能不适用于每个形状) )。

  • Do a morphological operation on your mask image to calculate the border, for instance with the vImageDilate family of functions (more complicated, and may run into performance problems).
  • 对掩码图像执行形态学操作以计算边界,例如使用vImageDilate函数系列(更复杂,可能会遇到性能问题)。

  • If you know the shape and it can be described mathematically, draw it and stroke it explicitly with Core Graphics functions.
  • 如果您知道形状并且可以用数学方法描述,请使用Core Graphics函数绘制并显式描边。

  • Or, in the same case (shape known mathematically), use a CAShapeLayer to draw the border.
  • 或者,在相同的情况下(数学上已知的形状),使用CAShapeLayer绘制边框。

#2


4  

Consider this example code:

考虑以下示例代码:

- (void)drawRect:(CGRect)rect {
    CAShapeLayer *maskLayer = [CAShapeLayer layer];

    //Modify to your needs
    CGFloat maskInsetWidth = 5.0f;
    CGFloat maskInsetHeight = 5.0f;
    CGFloat maskCornerRadius = 5.0f;
    CGFloat borderWidth = 2.0f;
    UIColor *borderColor = [UIColor blackColor];

    CGRect insetRect = CGRectInset(self.bounds, maskInsetWidth, maskInsetHeight);
    insetRect.size.width = MAX(insetRect.size.width, 0);
    insetRect.size.height = MAX(insetRect.size.height, 0);

    CGPathRef path = [UIBezierPath bezierPathWithRoundedRect:insetRect cornerRadius:maskCornerRadius].CGPath;

    if (borderWidth > 0.0f && borderColor != nil) {
        CAShapeLayer *borderLayer = [CAShapeLayer layer];

        [borderLayer setPath:path];
        [borderLayer setLineWidth:borderWidth * 2.0f];
        [borderLayer setStrokeColor:borderColor.CGColor];
        [borderLayer setFillColor:[UIColor clearColor].CGColor];

        borderLayer.frame = self.bounds;
        [self.layer addSublayer:borderLayer];
    }
    [maskLayer setPath:path];
    [maskLayer setFillRule:kCAFillRuleEvenOdd];
    maskLayer.frame = self.bounds;
    [self.layer setMask:maskLayer];
}

#3


2  

My approach in swift3.

我在swift3中的方法。

// Usage:
self.btnGroup.roundCorner([.topRight, .bottomRight], radius: 4.0, borderColor: UIColor.red, borderWidth: 1.0)

// Apply round corner and border. An extension method of UIView.
public func roundCorner(_ corners: UIRectCorner, radius: CGFloat, borderColor: UIColor, borderWidth: CGFloat) {
    let path = UIBezierPath.init(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))

    let mask = CAShapeLayer()
    mask.path = path.cgPath
    self.layer.mask = mask

    let borderPath = UIBezierPath.init(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
    let borderLayer = CAShapeLayer()
    borderLayer.path = borderPath.cgPath
    borderLayer.lineWidth = borderWidth
    borderLayer.strokeColor = borderColor.cgColor
    borderLayer.fillColor = UIColor.clear.cgColor
    borderLayer.frame = self.bounds
    self.layer.addSublayer(borderLayer)
}

#4


1  

In a general case you cannot easily set a border around a mask. That's like asking to put a border around the transparent pixels of an image. Perhaps it may be done using image filters. In some more specific case, if you are using plain CAShapeLayer then here is a sample of code that does that:

在一般情况下,您无法轻松地在蒙版周围设置边框。这就像要求在图像的透明像素周围放置边框。也许可以使用图像滤镜来完成。在一些更具体的情况下,如果您使用普通的CAShapeLayer,那么这里是一个代码示例:

[CATransaction begin];
[CATransaction setDisableActions:YES];

CALayer *hostLayer = [CALayer layer];
hostLayer.backgroundColor = [NSColor blackColor].CGColor;
hostLayer.speed  = 0.0;
hostLayer.timeOffset = 0.0;

CALayer *maskedLayer = [CALayer layer];
maskedLayer.backgroundColor = [NSColor redColor].CGColor;
maskedLayer.position = CGPointMake(200, 200);
maskedLayer.bounds   = CGRectMake(0, 0, 200, 200);

CAShapeLayer *mask = [CAShapeLayer layer];
mask.fillColor = [NSColor whiteColor].CGColor;
mask.position = CGPointMake(100, 100);
mask.bounds   = CGRectMake(0, 0, 200, 200);

CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 100, 100);
for (int i=0;  i<20;  i++) {
    double x = arc4random_uniform(2000) / 10.0;
    double y = arc4random_uniform(2000) / 10.0;
    CGPathAddLineToPoint(path, NULL, x, y);
}
CGPathCloseSubpath(path);

mask.path = path;

CGPathRelease(path);

maskedLayer.mask = mask;

CAShapeLayer *maskCopy = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:mask]];
maskCopy.fillColor = NULL;
maskCopy.strokeColor = [NSColor yellowColor].CGColor;
maskCopy.lineWidth = 4;
maskCopy.position = maskedLayer.position;

// Alternately, don't set the position and add the copy as a sublayer
// maskedLayer.sublayers = @[maskCopy];

hostLayer.sublayers = @[maskedLayer,maskCopy];

_contentView.layer = hostLayer;
_contentView.wantsLayer = YES;

[CATransaction commit];

It basically creates an arbitrary path and sets it as the mask. It then takes a copy of this layer to stroke the path. You might need to tweak things to get the exact effect you are looking for.

它基本上创建一个任意路径并将其设置为掩码。然后它需要此图层的副本来描边路径。您可能需要调整一些内容才能获得您想要的确切效果。

#5


1  

Swift 2 variation of Werner's answer

Swer 2对Werner答案的变化

如何在面具周围绘制CALayer边框?

//setup MASK
imageView.layer.mask = nil;
let cornerRadius = self.imageView.bounds.height * 0.5
let maskPath = UIBezierPath(roundedRect: self.imageView.bounds, byRoundingCorners: [.TopLeft, .BottomRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))

let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath
self.imageView.layer.mask = maskLayer

//setup Border for Mask
let maskInsetWidth :CGFloat = 0.0
let maskInsetHeight :CGFloat = 0.0

let borderWidth: CGFloat = self.imageView.bounds.width * 0.05
let borderColor = UIColor.redColor()

let insetRect = CGRectInset(imageView.bounds, maskInsetWidth, maskInsetHeight)
let path = UIBezierPath(roundedRect: insetRect, byRoundingCorners: [.TopLeft, .BottomRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)).CGPath

let borderLayer = CAShapeLayer()
borderLayer.path = path
borderLayer.lineWidth = borderWidth * 2.0
borderLayer.strokeColor = borderColor.CGColor
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.frame = imageView.bounds
imageView.layer.addSublayer(borderLayer)

#6


1  

If you subclass CALayer, you could instantiate it with the mask you want, and also override layoutSubLayers to include the border you want. This will work for all masks and should be the new accepted answer.

如果你是CALayer的子类,你可以用你想要的掩码实例化它,并覆盖layoutSubLayers以包含你想要的边框。这适用于所有面具,应该是新接受的答案。

Could do this a couple ways. Below Ill do it by using the path of the given mask, and assigning that to class property to be used for constructing the new border in layoutSubLayers. There is potential that this method could be called multiple times, so I also set a boolean to track this. (Could also assign the border as a class property, and remove/re-add each time. For now I use bool check.

可以通过几种方式做到这一点。在Ill下面,使用给定掩码的路径,并将其分配给class属性,用于在layoutSubLayers中构造新边框。有可能多次调用此方法,因此我还设置了一个布尔值来跟踪它。 (也可以将边框指定为类属性,并且每次都删除/重新添加。现在我使用bool检查。

Swift 3:

class CustomLayer: CALayer {

    private var path: CGPath?
    private var borderSet: Bool = false

    init(maskLayer: CAShapeLayer) {
        super.init()
        self.path = maskLayer.path
        self.frame = maskLayer.frame
        self.bounds = maskLayer.bounds
        self.mask = maskLayer
    }

    override func layoutSublayers() {

        if(!borderSet) {
            self.borderSet = true
            let newBorder = CAShapeLayer()

            newBorder.lineWidth = 12
            newBorder.path = self.path
            newBorder.strokeColor = UIColor.black.cgColor
            newBorder.fillColor = nil

            self.addSublayer(newBorder)
        }

    }

    required override init(layer: Any) {
        super.init(layer: layer)
    }

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

#1


4  

Some suggestions:

  • Use an opaque shadow instead of a border (you will have a blurred effect).
  • 使用不透明阴影而不是边框​​(您将产生模糊效果)。

  • Create another layer, set its background color with the color you want for your border, mask it with a mask slightly bigger than the one you already have to simulate the border width, and put it centered behind your layer (may not work with every shape).
  • 创建另一个图层,使用您想要的边框颜色设置其背景颜色,使用比模拟边框宽度稍大的面具遮住它,并将其置于图层后面(可能不适用于每个形状) )。

  • Do a morphological operation on your mask image to calculate the border, for instance with the vImageDilate family of functions (more complicated, and may run into performance problems).
  • 对掩码图像执行形态学操作以计算边界,例如使用vImageDilate函数系列(更复杂,可能会遇到性能问题)。

  • If you know the shape and it can be described mathematically, draw it and stroke it explicitly with Core Graphics functions.
  • 如果您知道形状并且可以用数学方法描述,请使用Core Graphics函数绘制并显式描边。

  • Or, in the same case (shape known mathematically), use a CAShapeLayer to draw the border.
  • 或者,在相同的情况下(数学上已知的形状),使用CAShapeLayer绘制边框。

#2


4  

Consider this example code:

考虑以下示例代码:

- (void)drawRect:(CGRect)rect {
    CAShapeLayer *maskLayer = [CAShapeLayer layer];

    //Modify to your needs
    CGFloat maskInsetWidth = 5.0f;
    CGFloat maskInsetHeight = 5.0f;
    CGFloat maskCornerRadius = 5.0f;
    CGFloat borderWidth = 2.0f;
    UIColor *borderColor = [UIColor blackColor];

    CGRect insetRect = CGRectInset(self.bounds, maskInsetWidth, maskInsetHeight);
    insetRect.size.width = MAX(insetRect.size.width, 0);
    insetRect.size.height = MAX(insetRect.size.height, 0);

    CGPathRef path = [UIBezierPath bezierPathWithRoundedRect:insetRect cornerRadius:maskCornerRadius].CGPath;

    if (borderWidth > 0.0f && borderColor != nil) {
        CAShapeLayer *borderLayer = [CAShapeLayer layer];

        [borderLayer setPath:path];
        [borderLayer setLineWidth:borderWidth * 2.0f];
        [borderLayer setStrokeColor:borderColor.CGColor];
        [borderLayer setFillColor:[UIColor clearColor].CGColor];

        borderLayer.frame = self.bounds;
        [self.layer addSublayer:borderLayer];
    }
    [maskLayer setPath:path];
    [maskLayer setFillRule:kCAFillRuleEvenOdd];
    maskLayer.frame = self.bounds;
    [self.layer setMask:maskLayer];
}

#3


2  

My approach in swift3.

我在swift3中的方法。

// Usage:
self.btnGroup.roundCorner([.topRight, .bottomRight], radius: 4.0, borderColor: UIColor.red, borderWidth: 1.0)

// Apply round corner and border. An extension method of UIView.
public func roundCorner(_ corners: UIRectCorner, radius: CGFloat, borderColor: UIColor, borderWidth: CGFloat) {
    let path = UIBezierPath.init(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))

    let mask = CAShapeLayer()
    mask.path = path.cgPath
    self.layer.mask = mask

    let borderPath = UIBezierPath.init(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
    let borderLayer = CAShapeLayer()
    borderLayer.path = borderPath.cgPath
    borderLayer.lineWidth = borderWidth
    borderLayer.strokeColor = borderColor.cgColor
    borderLayer.fillColor = UIColor.clear.cgColor
    borderLayer.frame = self.bounds
    self.layer.addSublayer(borderLayer)
}

#4


1  

In a general case you cannot easily set a border around a mask. That's like asking to put a border around the transparent pixels of an image. Perhaps it may be done using image filters. In some more specific case, if you are using plain CAShapeLayer then here is a sample of code that does that:

在一般情况下,您无法轻松地在蒙版周围设置边框。这就像要求在图像的透明像素周围放置边框。也许可以使用图像滤镜来完成。在一些更具体的情况下,如果您使用普通的CAShapeLayer,那么这里是一个代码示例:

[CATransaction begin];
[CATransaction setDisableActions:YES];

CALayer *hostLayer = [CALayer layer];
hostLayer.backgroundColor = [NSColor blackColor].CGColor;
hostLayer.speed  = 0.0;
hostLayer.timeOffset = 0.0;

CALayer *maskedLayer = [CALayer layer];
maskedLayer.backgroundColor = [NSColor redColor].CGColor;
maskedLayer.position = CGPointMake(200, 200);
maskedLayer.bounds   = CGRectMake(0, 0, 200, 200);

CAShapeLayer *mask = [CAShapeLayer layer];
mask.fillColor = [NSColor whiteColor].CGColor;
mask.position = CGPointMake(100, 100);
mask.bounds   = CGRectMake(0, 0, 200, 200);

CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 100, 100);
for (int i=0;  i<20;  i++) {
    double x = arc4random_uniform(2000) / 10.0;
    double y = arc4random_uniform(2000) / 10.0;
    CGPathAddLineToPoint(path, NULL, x, y);
}
CGPathCloseSubpath(path);

mask.path = path;

CGPathRelease(path);

maskedLayer.mask = mask;

CAShapeLayer *maskCopy = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:mask]];
maskCopy.fillColor = NULL;
maskCopy.strokeColor = [NSColor yellowColor].CGColor;
maskCopy.lineWidth = 4;
maskCopy.position = maskedLayer.position;

// Alternately, don't set the position and add the copy as a sublayer
// maskedLayer.sublayers = @[maskCopy];

hostLayer.sublayers = @[maskedLayer,maskCopy];

_contentView.layer = hostLayer;
_contentView.wantsLayer = YES;

[CATransaction commit];

It basically creates an arbitrary path and sets it as the mask. It then takes a copy of this layer to stroke the path. You might need to tweak things to get the exact effect you are looking for.

它基本上创建一个任意路径并将其设置为掩码。然后它需要此图层的副本来描边路径。您可能需要调整一些内容才能获得您想要的确切效果。

#5


1  

Swift 2 variation of Werner's answer

Swer 2对Werner答案的变化

如何在面具周围绘制CALayer边框?

//setup MASK
imageView.layer.mask = nil;
let cornerRadius = self.imageView.bounds.height * 0.5
let maskPath = UIBezierPath(roundedRect: self.imageView.bounds, byRoundingCorners: [.TopLeft, .BottomRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))

let maskLayer = CAShapeLayer()
maskLayer.frame = self.bounds;
maskLayer.path = maskPath.CGPath
self.imageView.layer.mask = maskLayer

//setup Border for Mask
let maskInsetWidth :CGFloat = 0.0
let maskInsetHeight :CGFloat = 0.0

let borderWidth: CGFloat = self.imageView.bounds.width * 0.05
let borderColor = UIColor.redColor()

let insetRect = CGRectInset(imageView.bounds, maskInsetWidth, maskInsetHeight)
let path = UIBezierPath(roundedRect: insetRect, byRoundingCorners: [.TopLeft, .BottomRight], cornerRadii: CGSize(width: cornerRadius, height: cornerRadius)).CGPath

let borderLayer = CAShapeLayer()
borderLayer.path = path
borderLayer.lineWidth = borderWidth * 2.0
borderLayer.strokeColor = borderColor.CGColor
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.frame = imageView.bounds
imageView.layer.addSublayer(borderLayer)

#6


1  

If you subclass CALayer, you could instantiate it with the mask you want, and also override layoutSubLayers to include the border you want. This will work for all masks and should be the new accepted answer.

如果你是CALayer的子类,你可以用你想要的掩码实例化它,并覆盖layoutSubLayers以包含你想要的边框。这适用于所有面具,应该是新接受的答案。

Could do this a couple ways. Below Ill do it by using the path of the given mask, and assigning that to class property to be used for constructing the new border in layoutSubLayers. There is potential that this method could be called multiple times, so I also set a boolean to track this. (Could also assign the border as a class property, and remove/re-add each time. For now I use bool check.

可以通过几种方式做到这一点。在Ill下面,使用给定掩码的路径,并将其分配给class属性,用于在layoutSubLayers中构造新边框。有可能多次调用此方法,因此我还设置了一个布尔值来跟踪它。 (也可以将边框指定为类属性,并且每次都删除/重新添加。现在我使用bool检查。

Swift 3:

class CustomLayer: CALayer {

    private var path: CGPath?
    private var borderSet: Bool = false

    init(maskLayer: CAShapeLayer) {
        super.init()
        self.path = maskLayer.path
        self.frame = maskLayer.frame
        self.bounds = maskLayer.bounds
        self.mask = maskLayer
    }

    override func layoutSublayers() {

        if(!borderSet) {
            self.borderSet = true
            let newBorder = CAShapeLayer()

            newBorder.lineWidth = 12
            newBorder.path = self.path
            newBorder.strokeColor = UIColor.black.cgColor
            newBorder.fillColor = nil

            self.addSublayer(newBorder)
        }

    }

    required override init(layer: Any) {
        super.init(layer: layer)
    }

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