| 1 | import ExpoModulesCore |
| 2 | import 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. |
| 6 | final 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 | } |