CALayer with transparent hole in it

后端 未结 5 597
误落风尘
误落风尘 2020-11-28 17:28

I have a simple view (left side of the picture) and i need to create some kind of overlay (right side of the picture) to this view. This overlay should have some opacity, so

相关标签:
5条回答
  • 2020-11-28 18:02

    Code Swift 2.0 compatible

    Starting from @animal_inch answer, I code a little utility-class, hope it will appreciate:

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

    Then, wherever in your code:

    let circle = CircleMaskView(drawIn: <target_view>)
    circle.opacity = 0.7
    circle.draw()
    
    0 讨论(0)
  • 2020-11-28 18:12

    I was able to solve this with Jon Steinmetz suggestion. If any one cares, here's the final solution:

    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)
    
    0 讨论(0)
  • 2020-11-28 18:13

    To create this effect, I found it easiest to create an entire view overlaying the screen, then subtracting portions of the screen using layers and UIBezierPaths. For a Swift implementation:

    // 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)
    

    I tested the code above, and here is the result:

    I added a library to CocoaPods that abstracts away a lot of the above code and allows you to easily create overlays with rectangular/circular holes, allowing the user to interact with views behind the overlay. I used it to create this tutorial for one of our apps:

    Tutorial using the TAOverlayView

    The library is called TAOverlayView, and is open source under Apache 2.0. I hope you find it useful!

    0 讨论(0)
  • 2020-11-28 18:16

    Accepted solution Swift 3.0 compatible

    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)
    
    0 讨论(0)
  • 2020-11-28 18:16

    I took a similar approach as animal_chin, but I'm more visual, so I set most of it up in Interface Builder using outlets and auto layout.

    Here is my solution in 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
    
    0 讨论(0)
提交回复
热议问题