CALayer ที่มีรูใสอยู่


112

ฉันมีมุมมองที่เรียบง่าย (ด้านซ้ายของภาพ) และฉันต้องการสร้างภาพซ้อนทับ (ด้านขวาของภาพ) ให้กับมุมมองนี้ ภาพซ้อนทับนี้ควรมีความทึบแสงดังนั้นการมองเห็นจึงยังคงมองเห็นได้บางส่วน ที่สำคัญที่สุดการวางซ้อนนี้ควรมีรูวงกลมตรงกลางเพื่อไม่ให้ซ้อนทับตรงกลางของมุมมอง (ดูภาพต่อไปนี้)

ฉันสามารถสร้างวงกลมได้ง่ายๆดังนี้:

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;

และภาพซ้อนทับแบบ "เต็ม" แบบนี้:

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

แต่ฉันไม่รู้ว่าจะรวมสองเลเยอร์นี้เข้าด้วยกันเพื่อสร้างเอฟเฟกต์ที่ฉันต้องการได้อย่างไร ทุกคน? ฉันลองทุกอย่างแล้วจริงๆ ... ขอบคุณมากสำหรับความช่วยเหลือ!

ภาพ


คุณสามารถสร้าง bezier หนึ่งอันซึ่งมี rect และวงกลมจากนั้นกฎการคดเคี้ยวที่ใช้ระหว่างการวาดจะสร้างรู (ฉันยังไม่ได้ลอง)
Wain

ฉันไม่รู้วิธีการทำ :)
animal_chin

สร้างด้วยรูปสี่เหลี่ยมผืนผ้าจากmoveToPointนั้นเพิ่มสี่เหลี่ยมผืนผ้าที่โค้งมน UIBezierPathตรวจสอบเอกสารสำหรับวิธีการที่นำเสนอโดย
Wain

ดูว่าคำถามที่คล้ายกันนี้และคำตอบช่วยได้ไหม: [Cut transparent hole in UIView] [1] [1]: stackoverflow.com/questions/9711248/…
dichen

ลองดูวิธีแก้ปัญหาของฉันที่นี่: stackoverflow.com/questions/14141081/… หวังว่านี่จะช่วยใครสักคนนะ
James Laurenstin

คำตอบ:


218

ฉันสามารถแก้ปัญหานี้ได้ด้วยคำแนะนำของ Jon Steinmetz หากมีใครสนใจนี่คือทางออกสุดท้าย:

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
เพื่อเพิ่มความยืดหยุ่นให้สร้างคลาสย่อยมุมมองของคุณ "IBDesignable" ง่ายมาก! ในการเริ่มต้นให้เสียบรหัสด้านบนลงในคำตอบที่ฉันให้สำหรับคำถามนี้: stackoverflow.com/questions/14141081/…
clozach

2
ในฐานะนักพัฒนา iOS มือใหม่ฉันใช้เวลาไม่กี่ชั่วโมงในการพยายามคิดว่าทำไมโค้ดนี้ถึงได้ผลลัพธ์ที่แปลก ในที่สุดฉันก็พบว่าชั้นย่อยที่เพิ่มเข้ามานั้นจะต้องถูกลบออกหากมีการคำนวณโอเวอร์เลย์มาสก์ใหม่ในบางจุด สามารถทำได้ผ่านคุณสมบัติ view.layer.sublayers ขอบคุณมากสำหรับคำตอบ!
Serzhas

ทำไมฉันถึงได้ตรงกันข้ามกับมัน ชั้นสีใสที่มีรูปร่างกึ่งโปร่งใสสีดำ ??
Chanchal Warde

ฉันจะเพิ่มข้อความโปร่งใสในวงกลมโดยใช้โหมดนี้ได้อย่างไร? ฉันไม่พบวิธีการ
Diego Fernando Murillo Valenci

เกือบ 6 ปียังช่วยได้ แต่โปรดจำไว้ว่ารูกลวงไม่ได้ 'เจาะทะลุ' ชั้นที่เก็บมันไว้ พูดว่าถ้าวางซ้อนรูทับปุ่ม ปุ่มนี้ไม่สามารถเข้าถึงได้ซึ่งจำเป็นหากคุณกำลังพยายามสร้าง 'คำแนะนำแบบแนะนำ' เหมือนฉัน ไลบรารีที่จัดทำโดย @Nick Yap จะทำงานให้คุณโดยการลบล้างจุด func (จุดภายใน: CGPoint ด้วยเหตุการณ์: UIEvent?) -> Bool {} ของ UIView ตรวจสอบห้องสมุดของเขาสำหรับรายละเอียดเพิ่มเติม แต่สิ่งที่คุณคาดหวังมีเพียงแค่ 'การมองเห็นสิ่งที่อยู่หลังหน้ากาก' นี่คือคำตอบที่ถูกต้อง
infinity_coding7

32

ในการสร้างเอฟเฟกต์นี้ฉันพบว่าง่ายที่สุดในการสร้างมุมมองทั้งหมดที่ซ้อนทับหน้าจอจากนั้นลบส่วนของหน้าจอโดยใช้เลเยอร์และ UIBezierPaths สำหรับการใช้งาน 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)

ฉันทดสอบโค้ดด้านบนและนี่คือผลลัพธ์:

ใส่คำอธิบายภาพที่นี่

ฉันได้เพิ่มไลบรารีลงใน CocoaPods ซึ่งแยกโค้ดด้านบนออกไปจำนวนมากและช่วยให้คุณสร้างภาพซ้อนทับด้วยรูสี่เหลี่ยม / วงกลมได้อย่างง่ายดายทำให้ผู้ใช้สามารถโต้ตอบกับมุมมองที่อยู่เบื้องหลังภาพซ้อนทับได้ ฉันใช้มันเพื่อสร้างบทช่วยสอนนี้สำหรับหนึ่งในแอปของเรา:

บทช่วยสอนโดยใช้ TAOverlayView

ไลบรารีนี้เรียกว่าTAOverlayViewและเป็นโอเพ่นซอร์สภายใต้ Apache 2.0 ฉันหวังว่าคุณพบว่ามีประโยชน์!


นอกจากนี้โปรดอย่าโพสต์คำตอบที่ซ้ำกัน ให้พิจารณาการดำเนินการอื่น ๆ ที่อาจช่วยให้ผู้ใช้ในอนาคตพบคำตอบที่ต้องการตามที่อธิบายไว้ในโพสต์ที่เชื่อมโยง เมื่อคำตอบเหล่านั้นแทบจะไม่เป็นมากกว่าลิงก์และคำแนะนำในการใช้ข้อมูลของคุณคำตอบเหล่านั้นก็ดูเหมือนเป็นสแปม
Mogsdad

1
@Mogsdad ฉันไม่อยากให้ดูเหมือนเป็นสแปมฉันเพิ่งใช้เวลาพอสมควรในห้องสมุดนี้และฉันคิดว่ามันจะมีประโยชน์กับคนที่พยายามทำสิ่งที่คล้ายกัน แต่ขอบคุณสำหรับคำติชมฉันจะอัปเดตคำตอบเพื่อใช้ตัวอย่างโค้ด
Nick Yap

3
อัพเดทดีครับพี่นิค ฉันอยู่ในมุมของคุณ - ฉันมีไลบรารีและยูทิลิตี้ที่เผยแพร่ด้วยตัวเองและฉันเข้าใจว่าอาจดูซ้ำซ้อนที่จะใส่คำตอบที่สมบูรณ์ที่นี่เมื่อเอกสารของฉันครอบคลุมอยู่แล้ว ... อย่างไรก็ตามแนวคิดคือการเก็บคำตอบไว้ในตัว เป็นไปได้. และมีเป็นคนโพสต์อะไร แต่สแปมดังนั้นฉันไม่อยากจะล้างโลกในกับพวกเขา ฉันคิดว่าคุณมีความคิดเดียวกันนั่นคือเหตุผลที่ฉันชี้ให้คุณเห็น ไชโย!
Mogsdad

ฉันใช้พ็อดที่คุณสร้างขึ้นขอบคุณสำหรับมัน แต่มุมมองของฉันที่อยู่ใต้ภาพซ้อนทับหยุดโต้ตอบ มันผิดอะไร? ฉันมี Scrollview ที่มี imageview อยู่ข้างใน
Ammar Mujeeb

@AmmarMujeeb โอเวอร์เลย์บล็อกการโต้ตอบยกเว้นผ่าน "รู" ที่คุณสร้างขึ้น ความตั้งใจของฉันกับพ็อดคือการวางซ้อนที่เน้นบางส่วนของหน้าจอและอนุญาตให้คุณโต้ตอบกับองค์ประกอบที่ไฮไลต์เท่านั้น
Nick Yap

11

โซลูชันที่ยอมรับเข้ากันได้กับ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: ลิงก์ของคุณตายแล้ว
Randy

10

ฉันใช้วิธีการคล้าย ๆ กับ animal_chin แต่ฉันเห็นภาพมากกว่าดังนั้นฉันจึงตั้งค่าส่วนใหญ่ในตัวสร้างอินเทอร์เฟซโดยใช้ร้านค้าและเค้าโครงอัตโนมัติ

นี่คือวิธีแก้ปัญหาของฉันใน 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

ฉันชอบโซลูชันนี้เพราะคุณสามารถย้าย CirclePath ไปรอบ ๆ ในเวลาออกแบบและเวลาทำงานได้อย่างง่ายดาย
Mark Moeykens

สิ่งนี้ไม่ได้ผลสำหรับฉันแม้ว่าฉันจะแก้ไขให้ใช้ rect ปกติแทนรูปไข่ แต่ภาพมาสก์สุดท้ายออกมาผิด :(
GameDev

7

เข้ากันได้กับ Code Swift 2.0

เริ่มจากคำตอบ @animal_inch ฉันเขียนโค้ดระดับยูทิลิตี้เล็กน้อยหวังว่ามันจะประทับใจ:

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

}

จากนั้นทุกที่ในรหัสของคุณ:

let circle = CircleMaskView(drawIn: <target_view>)
circle.opacity = 0.7
circle.draw()
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.