How to draw CALayer border around its mask?

后端 未结 6 1448
谎友^
谎友^ 2021-02-07 09:39

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 b

相关标签:
6条回答
  • 2021-02-07 09:46

    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];
    }
    
    0 讨论(0)
  • 2021-02-07 09:49

    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.
    0 讨论(0)
  • 2021-02-07 09:57

    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.

    0 讨论(0)
  • 2021-02-07 09:57

    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")
        }
    }
    
    0 讨论(0)
  • 2021-02-07 10:05

    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)
    }
    
    0 讨论(0)
  • 2021-02-07 10:09

    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)
    }
    
    0 讨论(0)
提交回复
热议问题