如何在UIVisualEffectView中实现透明镂空?效果反转问题求解
UIVisualEffectView 透明镂空效果反转问题解决方案
问题复现
实现了通过overCurrentContext模态样式弹出的UIViewController,页面添加UIVisualEffectView实现暗模糊效果,初始展示正常。尝试通过CAShapeLayer配合evenOdd填充规则给模糊视图添加指定矩形的透明镂空,初始实现代码如下:
class CoverView: UIView { private let blurView: UIVisualEffectView = { UIVisualEffectView(effect: UIBlurEffect(style: .dark)) }() // MARK: - Internal func setup() { addSubview(blurView) blurView.snp.makeConstraints { maker in maker.edges.equalToSuperview() } blurView.makeClearHole(rect: CGRect(x: 100, y: 100, width: 100, height: 230)) } } extension UIView { func makeClearHole(rect: CGRect) { let maskLayer = CAShapeLayer() maskLayer.fillRule = CAShapeLayerFillRule.evenOdd maskLayer.fillColor = UIColor.black.cgColor let pathToOverlay = UIBezierPath(rect: bounds) pathToOverlay.append(UIBezierPath(rect: rect)) pathToOverlay.usesEvenOddFillRule = true maskLayer.path = pathToOverlay.cgPath layer.mask = maskLayer } }
运行后效果与预期完全反转:预期效果为指定矩形区域透明、其余区域保持模糊;实际效果为指定矩形区域显示模糊,其余区域透明。
后续调整镂空实现代码,直接操作blurView的layer.mask,效果仍然没有改善:
private func makeClearHole(rect: CGRect) { let maskLayer = CAShapeLayer() maskLayer.fillColor = UIColor.black.cgColor let pathToOverlay = CGMutablePath() pathToOverlay.addRect(blurView.bounds) pathToOverlay.addRect(rect) maskLayer.path = pathToOverlay maskLayer.fillRule = .evenOdd blurView.layer.mask = maskLayer }
问题根源
效果反转的核心原因是调用镂空逻辑的时机错误:
- 代码在
setup()方法中刚给blurView添加完约束就执行镂空计算,此时Auto Layout还未完成布局流程,blurView.bounds的值为默认的CGRect.zero,并非视图最终显示的实际尺寸 - evenOdd填充规则下,路径重叠次数为奇数的区域会被判定为不透明(显示模糊效果),重叠次数为偶数的区域判定为透明。当外框路径是0尺寸矩形时,只有传入的镂空矩形区域被判定为不透明,其余区域全部透明,刚好和预期完全相反
- 初始生成mask路径后,没有监听视图布局变化更新路径,即使后续视图完成布局,mask仍然保持初始错误状态
正确实现方案
核心是等视图完成布局拿到真实bounds后再生成mask,同时在布局变化时自动更新mask路径,代码如下:
class CoverView: UIView { private let blurView: UIVisualEffectView = { UIVisualEffectView(effect: UIBlurEffect(style: .dark)) }() // 保存镂空矩形配置 private var targetHoleRect: CGRect = .zero override func layoutSubviews() { super.layoutSubviews() // 布局完成拿到真实bounds后更新mask updateBlurViewHoleMask() } func setup(holeRect: CGRect) { targetHoleRect = holeRect addSubview(blurView) blurView.snp.makeConstraints { maker in maker.edges.equalToSuperview() } } private func updateBlurViewHoleMask() { // 尺寸无效时不生成mask guard blurView.bounds.size != .zero else { return } let maskLayer = CAShapeLayer() maskLayer.fillRule = .evenOdd maskLayer.fillColor = UIColor.black.cgColor let maskPath = CGMutablePath() maskPath.addRect(blurView.bounds) maskPath.addRect(targetHoleRect) maskLayer.path = maskPath blurView.layer.mask = maskLayer } // 提供动态更新镂空区域的方法 func updateHoleRect(_ newRect: CGRect) { targetHoleRect = newRect setNeedsLayout() } }
注意事项
- 不要在
init、viewDidLoad、刚添加完约束的节点调用需要依赖视图bounds的绘制逻辑,此时视图尚未完成布局,拿到的尺寸不是最终显示尺寸 - 如果CoverView是在控制器中使用,若需要适配屏幕旋转、尺寸类变化,上述
layoutSubviews的更新逻辑会自动触发,不需要额外处理 - 不要将mask添加到
UIVisualEffectView的contentView上,直接给blurView本身的layer设置mask即可
内容的提问来源于stack exchange,提问作者kunass2
相关产品推荐
相关产品推荐

