CALayer có lỗ trong suốt


112

Tôi có một chế độ xem đơn giản (phía bên trái của hình ảnh) và tôi cần tạo một số loại lớp phủ (phía bên phải của bức ảnh) cho chế độ xem này. Lớp phủ này sẽ có một số độ mờ, vì vậy chế độ xem bên dưới nó vẫn có thể nhìn thấy một phần. Quan trọng nhất là lớp phủ này phải có một lỗ tròn ở giữa để nó không phủ lên tâm của chế độ xem (xem hình bên dưới).

Tôi có thể dễ dàng tạo một vòng kết nối như thế này:

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;

Và một lớp phủ hình chữ nhật "đầy đủ" như thế này:

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

Nhưng tôi không biết làm cách nào để kết hợp hai lớp này để chúng tạo ra hiệu ứng mà tôi muốn. Bất kỳ ai? Tôi thực sự đã thử mọi thứ ... Cảm ơn rất nhiều vì đã giúp đỡ!

Hình ảnh


Bạn có thể tạo một bezier chứa hình chữ nhật và hình tròn, sau đó quy tắc uốn lượn được sử dụng trong quá trình vẽ sẽ tạo ra một lỗ (tôi chưa thử).
Wain

tôi không biết làm thế nào để làm điều đó :)
thú_chin

Sau đó moveToPoint, tạo với hình chữ nhật, sau đó thêm hình chữ nhật tròn. Kiểm tra tài liệu để biết các phương pháp được cung cấp bởi UIBezierPath.
Wain

Xem câu hỏi và câu trả lời tương tự này có trợ giúp không: [Cắt lỗ trong suốt trong UIView] [1] [1]: stackoverflow.com/questions/9711248/…
dichen

Kiểm tra giải pháp của tôi ở đây: stackoverflow.com/questions/14141081/... Hy vọng rằng đây sẽ giúp người
James Laurenstin

Câu trả lời:


218

Tôi đã có thể giải quyết điều này với gợi ý của Jon Steinmetz. Nếu bất kỳ ai quan tâm, đây là giải pháp cuối cùng:

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
Để tăng tính linh hoạt, hãy đặt lớp con của chế độ xem của bạn là "IBDesignable". Nó thực sự dễ dàng! Để bắt đầu, cắm đoạn code trên vào câu trả lời tôi đã cho câu hỏi này: stackoverflow.com/questions/14141081/...
clozach

2
Là một nhà phát triển iOS mới làm quen, tôi đã dành vài giờ để cố gắng tìm hiểu, tại sao đoạn mã này lại tạo ra kết quả kỳ lạ. Cuối cùng, tôi thấy rằng các lớp con được thêm vào phải bị xóa nếu mặt nạ lớp phủ được tính toán lại tại một số điểm. Điều này có thể thực hiện được thông qua thuộc tính view.layer.sublayers. Cảm ơn bạn rất nhiều cho câu trả lời!
Serzhas

Tại sao tôi nhận được điều ngược lại hoàn toàn với nó. Lớp màu rõ ràng với hình dạng bán trong suốt màu đen ??
Chanchal Warde

Tôi có thể thêm văn bản trong suốt vào vòng kết nối bằng chế độ này như thế nào? tôi không tìm thấy như thế nào
Diego Fernando Murillo Valenci

Gần 6 năm, vẫn có ích, nhưng hãy nhớ rằng lỗ rỗng không thực sự 'xuyên thủng' lớp giữ nó. Giả sử, nếu chồng lỗ trên một nút. Nút này không thể truy cập được, cần thiết nếu bạn đang cố gắng thực hiện một 'hướng dẫn có hướng dẫn' như tôi. Thư viện do @Nick Yap cung cấp sẽ thực hiện công việc cho bạn, trong đó bằng cách ghi đè điểm func (điểm bên trong: CGPoint, với sự kiện: UIEvent?) -> Bool {} của UIView. Kiểm tra thư viện của anh ấy để biết thêm chi tiết. Nhưng, những gì bạn mong đợi chỉ là 'khả năng hiển thị những gì đằng sau mặt nạ', đây là một câu trả lời hợp lệ.
infinity_coding7

32

Để tạo hiệu ứng này, tôi thấy dễ nhất là tạo toàn bộ chế độ xem phủ lên màn hình, sau đó trừ các phần của màn hình bằng cách sử dụng các lớp và UIBezierPath. Để triển khai 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)

Tôi đã kiểm tra đoạn mã ở trên và đây là kết quả:

nhập mô tả hình ảnh ở đây

Tôi đã thêm một thư viện vào CocoaPods để tóm tắt rất nhiều mã ở trên và cho phép bạn dễ dàng tạo lớp phủ với các lỗ hình chữ nhật / hình tròn, cho phép người dùng tương tác với các chế độ xem phía sau lớp phủ. Tôi đã sử dụng nó để tạo hướng dẫn này cho một trong các ứng dụng của chúng tôi:

Hướng dẫn sử dụng TAOverlayView

Thư viện được gọi là TAOverlayView , và là mã nguồn mở trong Apache 2.0. Tôi hy vọng bạn thấy nó hữu dụng!


Ngoài ra, vui lòng không đăng các câu trả lời trùng lặp . Thay vào đó, hãy xem xét các hành động khác có thể giúp người dùng trong tương lai tìm thấy câu trả lời họ cần, như được mô tả trong bài đăng được liên kết. Khi những câu trả lời đó không chỉ là một liên kết và đề xuất sử dụng nội dung của bạn, chúng có vẻ khá spam.
Mogsdad

1
@Mogsdad Tôi không muốn xuất hiện spam, tôi chỉ dành một khoảng thời gian đáng kể cho thư viện này và tôi thấy nó sẽ hữu ích cho những người đang cố gắng làm những việc tương tự. Nhưng cảm ơn vì phản hồi, tôi sẽ cập nhật câu trả lời của mình để sử dụng các ví dụ về mã
Nick Yap

3
Cập nhật tốt, Nick. Tôi ở trong góc của bạn - bản thân tôi đã có các thư viện và tiện ích đã xuất bản và tôi hiểu rằng có vẻ thừa nếu đặt câu trả lời đầy đủ ở đây khi tài liệu của tôi đã bao gồm nó ... tuy nhiên, ý tưởng là giữ câu trả lời một cách khép kín càng tốt. Và có những người không đăng gì ngoài thư rác, vì vậy tôi không muốn bị gộp chung với họ. Tôi cho rằng bạn có cùng suy nghĩ, đó là lý do tại sao tôi chỉ ra điều đó cho bạn. Chúc mừng!
Mogsdad

Tôi đã sử dụng nhóm bạn đã tạo, cảm ơn vì nó. Nhưng các chế độ xem của tôi bên dưới lớp phủ ngừng tương tác. Có gì sai với nó? Tôi có một Scrollview với imageview bên trong nó.
Ammar Mujeeb 24/02/17

@AmmarMujeeb Lớp phủ chặn tương tác ngoại trừ các "lỗ hổng" mà bạn tạo. Ý định của tôi với nhóm là các lớp phủ làm nổi bật các phần của màn hình và chỉ cho phép bạn tương tác với các phần tử được đánh dấu.
Nick Yap

11

Giải pháp được chấp nhận Tương thích 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: liên kết của bạn đã chết
Randy

10

Tôi đã thực hiện một cách tiếp cận tương tự như animal_chin, nhưng tôi trực quan hơn, vì vậy tôi thiết lập hầu hết nó trong Trình tạo giao diện bằng cách sử dụng các cửa hàng và bố cục tự động.

Đây là giải pháp của tôi trong 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

Tôi thích giải pháp này vì bạn có thể di chuyển circlePath xung quanh thời điểm thiết kế và thời gian chạy rất dễ dàng.
Mark Moeykens

điều này không hiệu quả với tôi, mặc dù tôi đã sửa đổi nó để sử dụng hình chữ nhật bình thường thay vì hình bầu dục, nhưng hình ảnh mặt nạ cuối cùng đã xuất hiện sai :(
GameDev

7

Code Swift 2.0 tương thích

Bắt đầu từ câu trả lời @animal_inch, tôi viết mã một lớp tiện ích nhỏ, hy vọng nó sẽ đánh giá cao:

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

}

Sau đó, bất cứ nơi nào trong mã của bạn:

let circle = CircleMaskView(drawIn: <target_view>)
circle.opacity = 0.7
circle.draw()
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.