CALayer dengan lubang transparan di dalamnya


112

Saya memiliki tampilan sederhana (sisi kiri gambar) dan saya perlu membuat semacam overlay (sisi kanan gambar) untuk tampilan ini. Hamparan ini harus memiliki beberapa opasitas, jadi tampilan di bawahnya masih terlihat sebagian. Yang terpenting overlay ini harus memiliki lubang melingkar di tengahnya sehingga tidak menutupi bagian tengah tampilan (lihat gambar di bawah).

Saya dapat dengan mudah membuat lingkaran seperti ini:

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;

Dan hamparan persegi panjang "penuh" seperti ini:

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

Tetapi saya tidak tahu bagaimana saya bisa menggabungkan dua lapisan ini sehingga mereka menciptakan efek yang saya inginkan. Siapa saja? Aku sudah mencoba segalanya ... Terima kasih banyak atas bantuannya!

Gambar


Bisakah Anda membuat satu bezier yang berisi persegi dan lingkaran, lalu aturan belitan yang digunakan selama menggambar akan membuat lubang (saya belum mencobanya).
Wain

saya tidak tahu bagaimana melakukannya :)
animal_chin

Buat dengan persegi, lalu moveToPoint, lalu tambahkan persegi bulat. Periksa dokumen untuk metode yang ditawarkan oleh UIBezierPath.
Wain

Lihat apakah pertanyaan dan jawaban serupa ini membantu: [Potong lubang transparan di UIView] [1] [1]: stackoverflow.com/questions/9711248/…
dichen

Lihat solusi saya di sini: stackoverflow.com/questions/14141081/… Semoga ini membantu seseorang
James Laurenstin

Jawaban:


218

Saya bisa menyelesaikan ini dengan saran Jon Steinmetz. Jika ada yang peduli, inilah solusi akhirnya:

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
Untuk fleksibilitas tambahan, buat tampilan subclass Anda "IBDesignable". Sangat mudah! Untuk memulai,
masukkan

2
Sebagai pengembang iOS pemula, saya telah menghabiskan beberapa jam mencoba mencari tahu, mengapa kode ini menghasilkan hasil yang aneh. Akhirnya saya menemukan, bahwa sublapisan yang ditambahkan harus dihapus jika topeng overlay dihitung ulang di beberapa titik. Ini dimungkinkan melalui properti view.layer.sublayers. Terima kasih banyak atas jawabannya!
Serzhas

Mengapa saya mendapatkan kebalikan dari itu. Lapisan berwarna bening dengan bentuk semi transparan hitam ??
Chanchal Warde

Bagaimana saya bisa menambahkan teks transparan ke lingkaran menggunakan mode ini, apakah mungkin? saya tidak menemukan caranya
Diego Fernando Murillo Valenci

Hampir 6 tahun, masih membantu, tetapi perlu diingat bahwa lubang berlubang tidak benar-benar 'menembus' lapisan yang menahannya. Katakanlah, jika melapisi lubang di atas tombol. Tombol ini tidak dapat diakses, yang diperlukan jika Anda mencoba membuat 'tutorial terpandu' seperti saya. Pustaka yang disediakan oleh @Nick Yap akan melakukan tugasnya untuk Anda, di mana dengan mengganti titik fungsi (titik dalam: CGPoint, dengan peristiwa: UIEvent?) -> Bool {} dari UIView. Lihat perpustakaannya untuk lebih jelasnya. Tapi, yang Anda harapkan hanyalah 'visibilitas ke apa yang ada di balik topeng', ini adalah jawaban yang valid.
infinity_coding7

32

Untuk membuat efek ini, saya merasa paling mudah untuk membuat seluruh tampilan melapisi layar, kemudian mengurangkan bagian layar menggunakan lapisan dan UIBezierPaths. Untuk implementasi 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)

Saya menguji kode di atas, dan inilah hasilnya:

masukkan deskripsi gambar di sini

Saya menambahkan perpustakaan ke CocoaPods yang mengabstraksi banyak kode di atas dan memungkinkan Anda untuk dengan mudah membuat overlay dengan lubang persegi panjang / melingkar, memungkinkan pengguna untuk berinteraksi dengan tampilan di balik overlay. Saya menggunakannya untuk membuat tutorial ini untuk salah satu aplikasi kami:

Tutorial menggunakan TAOverlayView

Pustaka ini disebut TAOverlayView , dan open source di bawah Apache 2.0. Semoga bermanfaat!


Selain itu, jangan memposting jawaban duplikat . Sebagai gantinya, pertimbangkan tindakan lain yang dapat membantu pengguna di masa mendatang menemukan jawaban yang mereka butuhkan, seperti yang dijelaskan dalam postingan tertaut. Ketika jawaban-jawaban itu tidak lebih dari sekedar tautan & rekomendasi untuk menggunakan barang-barang Anda, jawaban itu tampak cukup berisi spam.
Mogsdad

1
@Mogsdad Saya tidak ingin terlihat seperti spam, saya hanya menghabiskan banyak waktu di perpustakaan ini dan saya pikir ini akan berguna bagi orang yang mencoba melakukan hal serupa. Namun terima kasih atas umpan baliknya, saya akan memperbarui jawaban saya untuk menggunakan contoh kode
Nick Yap

3
Pembaruan yang bagus, Nick. Saya ada di pihak Anda - saya sendiri memiliki perpustakaan dan utilitas yang diterbitkan, dan saya mengerti bahwa tampaknya berlebihan untuk memberikan jawaban lengkap di sini ketika dokumentasi saya sudah mencakupnya ... namun idenya adalah untuk menjaga jawaban tetap mandiri mungkin. Dan ada yang orang posting apa pun kecuali spam, jadi saya lebih suka tidak akan disamakan dengan mereka. Saya berasumsi bahwa Anda memiliki pikiran yang sama, itulah sebabnya saya menunjukkannya kepada Anda. Bersulang!
Mogsdad

Saya menggunakan pod yang Anda buat, terima kasih untuk itu. Tapi pandangan saya di bawah hamparan berhenti berinteraksi. Apakah ada yang salah? Saya memiliki Scrollview dengan imageview di dalamnya.
Ammar Mujeeb

@AmmarMujeeb Hamparan memblokir interaksi kecuali melalui "lubang" yang Anda buat. Maksud saya dengan pod adalah overlay yang menyoroti bagian layar, dan hanya memungkinkan Anda untuk berinteraksi dengan elemen yang disorot.
Nick Yap

11

Solusi yang diterima kompatibel dengan 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: link Anda sudah mati
Randy

10

Saya mengambil pendekatan yang mirip dengan animal_chin, tetapi saya lebih visual, jadi saya mengatur sebagian besar di Interface Builder menggunakan outlet dan tata letak otomatis.

Inilah solusi saya di 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

Saya menyukai solusi ini karena Anda dapat memindahkan circlePath pada waktu desain dan menjalankan waktu dengan sangat mudah.
Mark Moeykens

ini tidak berhasil untuk saya, meskipun saya memodifikasinya untuk menggunakan persegi normal, bukan oval, tetapi gambar topeng terakhir hanya salah :(
GameDev

7

Kompatibel dengan Kode Swift 2.0

Mulai dari jawaban @animal_inch, saya mengkodekan sedikit kelas utilitas, semoga akan menghargai:

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

}

Lalu, di mana pun dalam kode Anda:

let circle = CircleMaskView(drawIn: <target_view>)
circle.opacity = 0.7
circle.draw()
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.