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

Но я понятия не имею, как объединить эти два слоя, чтобы они создавали нужный мне эффект. Кто угодно? Перепробовала реально все ... Большое спасибо за помощь!

Образ

animal_chin
источник
Можете ли вы создать одну кривую Безье, которая содержит прямоугольник и круг, а затем правило обмотки, используемое во время рисования, создаст отверстие (я не пробовал).
Wain
я не знаю как это делать :)
animal_chin
Затем создайте прямоугольник, moveToPointзатем добавьте прямоугольник с закругленными углами. Проверьте документацию на методы, предлагаемые UIBezierPath.
Wain
Посмотрите, поможет ли этот аналогичный вопрос и ответ: [Вырезать прозрачное отверстие в UIView] [1] [1]: stackoverflow.com/questions/9711248/…
dichen
Ознакомьтесь с моим решением здесь: stackoverflow.com/questions/14141081/… Надеюсь, это кому-то поможет,
Джеймс Лоренстин

Ответы:

218

Я смог решить эту проблему с помощью предложения Джона Стейнмеца. Если кому-то интересно, вот окончательное решение:

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)
animal_chin
источник
2
Для дополнительной гибкости сделайте свой подкласс представления «IBDesignable». Это действительно просто! Для начала вставьте приведенный выше код в ответ, который я дал на этот вопрос: stackoverflow.com/questions/14141081/…
clozach
2
Как начинающий разработчик iOS, я потратил несколько часов, пытаясь понять, почему этот код дает странные результаты. Наконец, я обнаружил, что добавленные подслои должны быть удалены, если маска наложения пересчитывается в какой-то момент. Это возможно через свойство view.layer.sublayers. Большое спасибо за ответ!
Сержас 04
Почему я получаю прямо противоположное. Прозрачный цветной слой с черной полупрозрачной формой ??
Chanchal Warde
Как в этом режиме можно добавить прозрачный текст в круг? Я не понимаю, как
Диего Фернандо Мурильо Валенси
Почти 6 лет, все еще помогает, но имейте в виду, что полое отверстие на самом деле не «пробивает» слой, который его удерживает. Скажем, если наложить дырочку на пуговицу. Кнопка недоступна, что необходимо, если вы пытаетесь создать «управляемое руководство», как я. Библиотека, предоставленная @Nick Yap, сделает эту работу за вас, переопределив точку функции (внутренняя точка: 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 Я не хотел показаться спамом, я просто потратил много времени на эту библиотеку и решил, что она будет полезна людям, пытающимся делать подобные вещи. Но спасибо за обратную связь, я обновлю свои ответы, чтобы использовать примеры кода
Ник Яп
3
Хорошее обновление, Ник. Я нахожусь в вашем углу - у меня есть опубликованные библиотеки и утилиты, и я понимаю, что может показаться излишним помещать здесь полные ответы, когда моя документация уже охватывает это ... однако идея состоит в том, чтобы ответы оставались самодостаточными насколько возможно. И есть люди не проводки ничего , кроме спама, так что я бы не быть , а сосредоточенные с ними. Я предполагаю, что вы того же мнения, поэтому я указал вам на это. Ура!
Mogsdad
Я использовал созданный вами модуль, спасибо за это. Но мои взгляды под наложением перестают взаимодействовать. Что с этим не так? У меня есть Scrollview с изображением внутри него.
Аммар Муджиб,
@AmmarMujeeb Оверлей блокирует взаимодействие, кроме как через «дыры», которые вы создаете. Моим намерением в отношении модуля были наложения, которые выделяют части экрана и позволяют вам взаимодействовать только с выделенными элементами.
Ник Яп,
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: твоя ссылка мертва
Рэнди
10

Я использовал тот же подход, что и animal_chin, но я более нагляден, поэтому я установил большую часть его в Interface Builder, используя розетки и автоматический макет.

Вот мое решение в 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 во время разработки и выполнения.
Марк Мойкенс
это не сработало для меня, хотя я изменил его, чтобы использовать нормальный прямоугольник вместо овала, но окончательное изображение маски просто выходит неправильно :(
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()
Лука Даванцо
источник