Files
Swiftgram/submodules/TelegramUI/Components/LensTransition/Sources/LensTransitionContainer.swift
2026-02-20 09:57:49 +01:00

302 lines
15 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
@inline(__always)
private func getMethod<T>(object: NSObject, selector: String) -> T? {
guard let method = object.method(for: NSSelectorFromString(selector)) else {
return nil
}
return unsafeBitCast(method, to: T.self)
}
private var cachedClasses: [String: NSObject] = [:]
private func getAndCacheClass(name: String) -> NSObject? {
if let value = cachedClasses[name] {
return value
} else {
if let value = NSClassFromString(name as String) as AnyObject as? NSObject {
cachedClasses[name] = value
return value
} else {
return nil
}
}
}
private var cachedAllocMethods: [String: (@convention(c) (AnyObject, Selector) -> NSObject?, Selector)] = [:]
private func invokeAllocMethod(className: String) -> NSObject? {
guard let classObject = getAndCacheClass(name: className) else {
return nil
}
if let cachedMethod = cachedAllocMethods[className] {
return cachedMethod.0(classObject, cachedMethod.1)
} else {
let method: (@convention(c) (AnyObject, Selector) -> NSObject?)? = getMethod(object: classObject, selector: "alloc")
if let method {
let selector = NSSelectorFromString("alloc")
cachedAllocMethods[className] = (method, selector)
return method(classObject, selector)
} else {
return nil
}
}
}
private var cachedInitMethods: [String: (@convention(c) (AnyObject, Selector) -> NSObject?, Selector)] = [:]
private func invokeInitMethod(className: String, object: NSObject) -> NSObject? {
if let cachedInitMethod = cachedInitMethods[className] {
return cachedInitMethod.0(object, cachedInitMethod.1)
} else {
let method: (@convention(c) (AnyObject, Selector) -> NSObject?)? = getMethod(object: object, selector: "init")
if let method {
let selector = NSSelectorFromString("init")
cachedInitMethods[className] = (method, selector)
return method(object, selector)
} else {
return nil
}
}
}
private func createObject(className: String) -> NSObject? {
if let object = invokeAllocMethod(className: className) {
return invokeInitMethod(className: className, object: object)
} else {
return nil
}
}
private func setFilterName(object: NSObject, name: String) {
object.perform(NSSelectorFromString("setName:"), with: name)
}
private final class EmptyLayerDelegate: NSObject, CALayerDelegate {
func action(for layer: CALayer, forKey event: String) -> CAAction? {
return nullAction
}
}
public final class LensTransitionContainer: UIView {
public enum State {
case animatedOut
case animatedIn
}
public let contentsView: UIView
private let emptyLayerDelegate = EmptyLayerDelegate()
private let sdfElementLayer: CALayer?
private let sdfLayer: CALayer?
private let displacementEffect: NSObject?
private(set) var state: State = .animatedOut
override public init(frame: CGRect) {
self.contentsView = UIView()
if #available(iOS 26.0, *) {
self.sdfElementLayer = createObject(className: ("CAS" as NSString).appending("DFElementLayer") as String) as? CALayer
self.sdfLayer = createObject(className: ("CAS" as NSString).appending("DFLayer")) as? CALayer
self.displacementEffect = createObject(className: ("CAS" as NSString).appending("DFGlassDisplacementEffect"))
} else {
self.sdfElementLayer = nil
self.sdfLayer = nil
self.displacementEffect = nil
}
super.init(frame: frame)
self.clipsToBounds = true
self.addSubview(self.contentsView)
//self.contentsView.backgroundColor = .blue
let curvature: CGFloat = 1.0
if let displacementEffect = self.displacementEffect {
displacementEffect.setValue(curvature, forKey: "curvature")
displacementEffect.setValue(0.0 as NSNumber, forKey: "angle")
}
if let sdfLayer = self.sdfLayer, let displacementEffect = self.displacementEffect {
sdfLayer.name = "sdfLayer"
sdfLayer.setValue(3.0, forKey: "scale")
sdfLayer.setValue(displacementEffect, forKey: "effect")
sdfLayer.delegate = self.emptyLayerDelegate
}
if let sdfLayer = self.sdfLayer, let sdfElementLayer = self.sdfElementLayer {
sdfElementLayer.setValue(0.0 as NSNumber, forKey: "gradientOvalization")
sdfElementLayer.isOpaque = true
sdfElementLayer.allowsEdgeAntialiasing = true
let sdfLayerDelegate = unsafeBitCast(sdfLayer, to: CALayerDelegate.self)
sdfElementLayer.delegate = sdfLayerDelegate
sdfElementLayer.setValue(UIScreenScale, forKey: "scale")
sdfLayer.addSublayer(sdfElementLayer)
}
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setIsFilterActive(isFilterActive: Bool) {
if isFilterActive {
if self.contentsView.layer.filters == nil {
if let sdfLayer = self.sdfLayer {
self.contentsView.layer.insertSublayer(sdfLayer, at: 0)
}
if let displacementFilter = CALayer.displacementMap(), let blurFilter = CALayer.blur() {
setFilterName(object: blurFilter, name: "gaussianBlur")
blurFilter.setValue(true, forKey: "inputNormalizeEdgesTransparent")
setFilterName(object: displacementFilter, name: "displacementMap")
displacementFilter.setValue("sdfLayer", forKey: "inputSourceSublayerName")
self.contentsView.layer.filters = [
blurFilter,
displacementFilter
]
}
}
} else if self.contentsView.layer.filters != nil {
self.contentsView.layer.filters = nil
if let sdfLayer = self.sdfLayer {
sdfLayer.removeFromSuperlayer()
}
}
}
public func update(size: CGSize, cornerRadius: CGFloat, state: State, transition: ComponentTransition) {
let previousState = self.state
self.state = state
transition.setFrame(view: self.contentsView, frame: CGRect(origin: CGPoint(), size: size))
if let sdfLayer = self.sdfLayer, let sdfElementLayer = self.sdfElementLayer, sdfLayer.bounds.size != size || previousState != state {
let previousSize = sdfLayer.bounds.size
if previousState == .animatedOut && state == .animatedIn && !transition.animation.isImmediate && previousSize != .zero {
self.setIsFilterActive(isFilterActive: true)
self.animateIn(fromSize: previousSize, fromCornerRadius: sdfLayer.cornerRadius, toSize: size, toCornerRadius: cornerRadius, transition: transition)
} else {
transition.setFrame(layer: sdfLayer, frame: CGRect(origin: CGPoint(), size: size), completion: { [weak self] _ in
guard let self else {
return
}
self.setIsFilterActive(isFilterActive: false)
})
transition.setCornerRadius(layer: sdfLayer, cornerRadius: cornerRadius)
transition.setFrame(layer: sdfElementLayer, frame: CGRect(origin: CGPoint(), size: size))
transition.setCornerRadius(layer: sdfElementLayer, cornerRadius: cornerRadius)
transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius)
let phase3Height: CGFloat = 0.0
let phase3Amount: CGFloat = -0.001
let phase3Blur: CGFloat = 0.0
self.contentsView.layer.setValue(phase3Height as NSNumber, forKeyPath: "sublayers.sdfLayer.effect.height")
self.contentsView.layer.setValue(phase3Amount as NSNumber, forKeyPath: "filters.displacementMap.inputAmount")
self.contentsView.layer.setValue(phase3Blur as NSNumber, forKeyPath: "filters.gaussianBlur.inputRadius")
}
}
}
private func animateIn(fromSize: CGSize, fromCornerRadius: CGFloat, toSize: CGSize, toCornerRadius: CGFloat, transition: ComponentTransition) {
guard let sdfLayer = self.sdfLayer, let sdfElementLayer = self.sdfElementLayer else {
return
}
let duration: Double
let transitionTimingFunction: String
if case let .curve(durationValue, _) = transition.animation {
duration = durationValue
transitionTimingFunction = kCAMediaTimingFunctionSpring
} else {
return
}
let firstPartDuration: Double = 0.35
let timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
let phase1Height: CGFloat = fromCornerRadius * 0.5
let phase1Amount: CGFloat = -phase1Height
let phase1Scale: CGFloat = 1.05
let phase2Height: CGFloat = min(toSize.width, toSize.height) / 3.3333
let phase2Amount: CGFloat = -phase2Height
let phase2Blur: CGFloat = 4.0
let phase2Scale: CGFloat = 1.05
let phase3Height: CGFloat = 0.0
let phase3Amount: CGFloat = -0.001
let phase3Blur: CGFloat = 0.0
let phase3Scale: CGFloat = 1.0
let capsuleCornerRadius = min(toSize.width, toSize.height) * 0.5
let fromCenter = CGPoint(x: fromSize.width * 0.5, y: fromSize.height * 0.5)
let toCenter = CGPoint(x: toSize.width * 0.5, y: toSize.height * 0.5)
// --- Phase 1: circle capsule, glass phase1 phase2 ---
let finalFrame = CGRect(origin: CGPoint(), size: toSize)
sdfLayer.frame = finalFrame
sdfElementLayer.frame = finalFrame
self.contentsView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
self.contentsView.layer.removeAllAnimations()
sdfLayer.cornerRadius = capsuleCornerRadius
sdfElementLayer.cornerRadius = capsuleCornerRadius
self.layer.cornerRadius = capsuleCornerRadius
self.contentsView.layer.setValue(phase2Height as NSNumber, forKeyPath: "sublayers.sdfLayer.effect.height")
self.contentsView.layer.setValue(phase2Amount as NSNumber, forKeyPath: "filters.displacementMap.inputAmount")
self.contentsView.layer.setValue(phase2Scale as NSNumber, forKeyPath: "transform.scale")
self.contentsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * firstPartDuration)
let phase1Duration = duration * firstPartDuration
let phase2Duration = duration * (1.0 - firstPartDuration)
let phase2BlurDuration = phase2Duration * 0.2
let _ = phase2BlurDuration
let _ = phase3Blur
self.contentsView.layer.setValue(0.0 as NSNumber, forKeyPath: "filters.gaussianBlur.inputRadius")
self.contentsView.layer.animate(from: phase2Blur as NSNumber, to: phase3Blur as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, duration: phase2BlurDuration, delay: phase1Duration * 0.8)
for layer in [sdfLayer, sdfElementLayer] {
layer.animate(from: NSValue(cgSize: fromSize), to: NSValue(cgSize: toSize), keyPath: "bounds.size", timingFunction: transitionTimingFunction, duration: duration)
layer.removeAnimation(forKey: "position")
layer.animate(from: fromCornerRadius as NSNumber, to: capsuleCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: timingFunction, duration: phase1Duration)
}
self.contentsView.layer.animate(from: NSValue(cgPoint: fromCenter), to: NSValue(cgPoint: toCenter), keyPath: "position", timingFunction: transitionTimingFunction, duration: duration)
self.contentsView.layer.animate(from: phase1Height as NSNumber, to: phase2Height as NSNumber, keyPath: "sublayers.sdfLayer.effect.height", timingFunction: timingFunction, duration: phase1Duration)
self.contentsView.layer.animate(from: phase1Amount as NSNumber, to: phase2Amount as NSNumber, keyPath: "filters.displacementMap.inputAmount", timingFunction: timingFunction, duration: phase1Duration)
self.contentsView.layer.animate(from: phase1Scale as NSNumber, to: phase2Scale as NSNumber, keyPath: "transform.scale", timingFunction: timingFunction, duration: phase1Duration)
self.layer.animate(from: fromCornerRadius as NSNumber, to: capsuleCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: timingFunction, duration: phase1Duration)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + UIView.animationDurationFactor() * phase1Duration, execute: { [weak self] in
guard let self else { return }
// --- Phase 2: capsule final cornerRadius, glass phase2 phase3 ---
sdfLayer.cornerRadius = toCornerRadius
sdfElementLayer.cornerRadius = toCornerRadius
self.layer.cornerRadius = toCornerRadius
self.contentsView.layer.setValue(phase3Height as NSNumber, forKeyPath: "sublayers.sdfLayer.effect.height")
self.contentsView.layer.setValue(phase3Amount as NSNumber, forKeyPath: "filters.displacementMap.inputAmount")
self.contentsView.layer.setValue(phase3Scale as NSNumber, forKeyPath: "transform.scale")
for layer in [sdfLayer, sdfElementLayer] {
layer.animate(from: capsuleCornerRadius as NSNumber, to: toCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: phase2Duration)
}
self.contentsView.layer.animate(from: phase2Height as NSNumber, to: phase3Height as NSNumber, keyPath: "sublayers.sdfLayer.effect.height", timingFunction: kCAMediaTimingFunctionSpring, duration: phase2Duration)
self.contentsView.layer.animate(from: phase2Amount as NSNumber, to: phase3Amount as NSNumber, keyPath: "filters.displacementMap.inputAmount", timingFunction: kCAMediaTimingFunctionSpring, duration: phase2Duration)
self.contentsView.layer.animate(from: phase2Scale as NSNumber, to: phase3Scale as NSNumber, keyPath: "transform.scale", timingFunction: kCAMediaTimingFunctionSpring, duration: phase2Duration)
self.layer.animate(from: capsuleCornerRadius as NSNumber, to: toCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: phase2Duration, completion: { [weak self] _ in
guard let self else { return }
self.setIsFilterActive(isFilterActive: false)
})
})
}
}