Files
mobile-ios/Rosetta/Features/Settings/DynamicIslandBlurView.swift

149 lines
4.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import UIKit
/// Telegram-parity Dynamic Island blur effect.
/// Replicates DynamicIslandBlurNode.swift from Telegram iOS:
/// - UIVisualEffectView(.dark) with UIViewPropertyAnimator for progressive blur
/// - Black fade overlay with alpha formula
/// - Radial gradient for edge feathering
struct DynamicIslandBlurView: UIViewRepresentable {
let progress: CGFloat
func makeUIView(context: Context) -> DynamicIslandBlurUIView {
DynamicIslandBlurUIView()
}
func updateUIView(_ uiView: DynamicIslandBlurUIView, context: Context) {
uiView.update(progress)
}
}
final class DynamicIslandBlurUIView: UIView {
private var effectView: UIVisualEffectView?
private let fadeView = UIView()
private let gradientView = UIImageView()
private var animator: UIViewPropertyAnimator?
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
isUserInteractionEnabled = false
clipsToBounds = true
// Blur effect view (Telegram: effectView with nil initial effect)
let effectView = UIVisualEffectView(effect: nil)
effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.effectView = effectView
addSubview(effectView)
// Radial gradient (Telegram: 100×100, center offset +38, radius 90)
gradientView.image = Self.makeGradientImage()
gradientView.contentMode = .center
gradientView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin]
addSubview(gradientView)
// Fade overlay (Telegram: black, alpha driven by formula)
fadeView.backgroundColor = .black
fadeView.alpha = 0
fadeView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(fadeView)
}
override func layoutSubviews() {
super.layoutSubviews()
effectView?.frame = bounds
fadeView.frame = bounds
let gradientSize = CGSize(width: 100, height: 100)
gradientView.frame = CGRect(
x: (bounds.width - gradientSize.width) / 2,
y: 0,
width: gradientSize.width,
height: gradientSize.height
)
}
func update(_ value: CGFloat) {
// Telegram formula: fadeAlpha = min(1.0, max(0.0, -0.25 + value * 1.55))
let fadeAlpha = min(1.0, max(0.0, -0.25 + value * 1.55))
if value > 0.0 {
var adjustedValue = value
let prepared = prepare()
if adjustedValue > 0.99 && prepared {
adjustedValue = 0.99
}
// Telegram formula: fractionComplete = max(0.0, -0.1 + value * 1.1)
animator?.fractionComplete = max(0.0, -0.1 + adjustedValue * 1.1)
} else {
animator?.stopAnimation(true)
animator = nil
effectView?.effect = nil
}
fadeView.alpha = fadeAlpha
}
private func prepare() -> Bool {
guard animator == nil else { return false }
let anim = UIViewPropertyAnimator(duration: 1.0, curve: .linear)
animator = anim
effectView?.effect = nil
anim.addAnimations { [weak self] in
self?.effectView?.effect = UIBlurEffect(style: .dark)
}
return true
}
deinit {
animator?.stopAnimation(true)
}
// Telegram: radial gradient 100×100, center (50, 88), radius 90
// Colors: transparent transparent (0.87) black (1.0)
private static func makeGradientImage() -> UIImage? {
let size = CGSize(width: 100, height: 100)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let ctx = UIGraphicsGetCurrentContext() else { return nil }
let colorSpace = CGColorSpaceCreateDeviceRGB()
var locations: [CGFloat] = [0.0, 0.87, 1.0]
let colors: [CGColor] = [
UIColor(white: 0, alpha: 0).cgColor,
UIColor(white: 0, alpha: 0).cgColor,
UIColor(white: 0, alpha: 1).cgColor,
]
guard let gradient = CGGradient(
colorsSpace: colorSpace,
colors: colors as CFArray,
locations: &locations
) else {
UIGraphicsEndImageContext()
return nil
}
let center = CGPoint(x: size.width / 2, y: size.height / 2 + 38)
ctx.drawRadialGradient(
gradient,
startCenter: center,
startRadius: 0,
endCenter: center,
endRadius: 90,
options: .drawsAfterEndLocation
)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}