1import ExpoModulesCore
2import UIKit
3
4/// One continuous blur with a rounded-rect hole, horizontally centered. A
5/// single masked effect view avoids the seams of tiling separate blurs.
6final class T3FrostedCutoutView: ExpoView {
7 var cutoutTop: CGFloat = 0 { didSet { setNeedsLayout() } }
8 var cutoutWidth: CGFloat = 0 { didSet { setNeedsLayout() } }
9 var cutoutHeight: CGFloat = 0 { didSet { setNeedsLayout() } }
10 var cutoutRadius: CGFloat = 0 { didSet { setNeedsLayout() } }
11 /// Follows the app theme, which can differ from the system appearance.
12 var appearance: String = "dark" {
13 didSet {
14 effectView.effect = UIBlurEffect(
15 style: appearance == "light" ? .systemThinMaterialLight : .systemThinMaterialDark)
16 }
17 }
18
19 private let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterialDark))
20 private let cutoutMaskView = UIView()
21 private let maskLayer = CAShapeLayer()
22
23 required init(appContext: AppContext? = nil) {
24 super.init(appContext: appContext)
25 isUserInteractionEnabled = false
26 maskLayer.fillRule = .evenOdd
27 cutoutMaskView.layer.addSublayer(maskLayer)
28 effectView.mask = cutoutMaskView
29 addSubview(effectView)
30 }
31
32 override func layoutSubviews() {
33 super.layoutSubviews()
34 effectView.frame = bounds
35 cutoutMaskView.frame = bounds
36 maskLayer.frame = bounds
37 let path = UIBezierPath(rect: bounds)
38 if cutoutWidth > 0, cutoutHeight > 0 {
39 let hole = CGRect(
40 x: (bounds.width - cutoutWidth) / 2,
41 y: cutoutTop,
42 width: cutoutWidth,
43 height: cutoutHeight
44 )
45 path.append(UIBezierPath(roundedRect: hole, cornerRadius: cutoutRadius))
46 }
47 maskLayer.path = path.cgPath
48 }
49}