How to draw CALayer border around its mask?

2020-06-01 09:00发布

问题:

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.

Can anyone please help me to solve this problem?

回答1:

Swift 4

class CustomView: UIView {
override func draw(_ rect: CGRect) {
    super.draw(rect)
    self.backgroundColor = UIColor.black

    //setup path for mask and border
    let halfHeight = self.bounds.height * 0.5
    let maskPath = UIBezierPath(roundedRect: self.bounds,
                                byRoundingCorners: [.topLeft, .bottomRight],
                                cornerRadii: CGSize(width: halfHeight,
                                                    height: halfHeight))

    //setup MASK
    self.layer.mask = nil;
    let maskLayer = CAShapeLayer()
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.cgPath
    self.layer.mask = maskLayer

    //setup Border for Mask
    let borderLayer = CAShapeLayer()
    borderLayer.path = maskPath.cgPath
    borderLayer.lineWidth = 25
    borderLayer.strokeColor = UIColor.red.cgColor
    borderLayer.fillColor = UIColor.clear.cgColor
    borderLayer.frame = self.bounds
    self.layer.addSublayer(borderLayer)
}


回答2:

My approach in 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)
}


回答3:

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).
  • If you know the shape and it can be described mathematically, draw it and stroke it explicitly with Core Graphics functions.
  • Or, in the same case (shape known mathematically), use a CAShapeLayer to draw the border.


回答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];
}


回答5:

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:

[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.



回答6:

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.

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.

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")
    }
}