mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
5554538444
@ -10575,12 +10575,19 @@ Sorry for the inconvenience.";
|
|||||||
"ChannelReactions.UnsavedChangesAlertDiscard" = "Discard";
|
"ChannelReactions.UnsavedChangesAlertDiscard" = "Discard";
|
||||||
"ChannelReactions.UnsavedChangesAlertApply" = "Apply";
|
"ChannelReactions.UnsavedChangesAlertApply" = "Apply";
|
||||||
"ChannelReactions.ToastMaxReactionsReached" = "You can select at most 100 reactions.";
|
"ChannelReactions.ToastMaxReactionsReached" = "You can select at most 100 reactions.";
|
||||||
"ChannelReactions.ToastLevelBoostRequired" = "Your channel needs to reach **Level %1$@** to add **%2$@** custom emoji as reactions.**";
|
|
||||||
|
"ChannelReactions.ToastLevelBoostRequiredTemplate" = "Your channel needs to reach **%1$@** to add **%2$@** as reactions.**";
|
||||||
|
"ChannelReactions.ToastLevelBoostRequiredTemplateLevel_1" = "Level 1";
|
||||||
|
"ChannelReactions.ToastLevelBoostRequiredTemplateLevel_any" = "Level %d";
|
||||||
|
"ChannelReactions.ToastLevelBoostRequiredTemplateEmojiCount_1" = "1 custom emoji";
|
||||||
|
"ChannelReactions.ToastLevelBoostRequiredTemplateEmojiCount_any" = "%d custom emoji";
|
||||||
|
|
||||||
"ChannelReactions.GeneralInfoLabel" = "You can add emoji from any emoji pack as a reaction.";
|
"ChannelReactions.GeneralInfoLabel" = "You can add emoji from any emoji pack as a reaction.";
|
||||||
"ChannelReactions.ReactionsSectionTitle" = "AVAILABLE REACTIONS";
|
"ChannelReactions.ReactionsSectionTitle" = "AVAILABLE REACTIONS";
|
||||||
"ChannelReactions.ReactionsInfoLabel" = "You can also [create your own]() emoji packs and use them.";
|
"ChannelReactions.ReactionsInfoLabel" = "You can also [create your own]() emoji packs and use them.";
|
||||||
"ChannelReactions.SaveAction" = "Update Reactions";
|
"ChannelReactions.SaveAction" = "Update Reactions";
|
||||||
"ChannelReactions.LevelRequiredLabel" = "Level %1$@ Required";
|
"ChannelReactions.LevelRequiredLabel" = "Level %1$@ Required";
|
||||||
|
"ChannelReactions.InputPlaceholder" = "Add Reactions...";
|
||||||
|
|
||||||
"ProfileColorSetup.ResetAction" = "Reset Profile Color";
|
"ProfileColorSetup.ResetAction" = "Reset Profile Color";
|
||||||
"ProfileColorSetup.IconSectionTitle" = "ADD ICON TO PROFILE";
|
"ProfileColorSetup.IconSectionTitle" = "ADD ICON TO PROFILE";
|
||||||
|
Binary file not shown.
BIN
Tests/CallUITest/Resources/test20.mp4
Normal file
BIN
Tests/CallUITest/Resources/test20.mp4
Normal file
Binary file not shown.
BIN
Tests/CallUITest/Resources/test3.mp4
Normal file
BIN
Tests/CallUITest/Resources/test3.mp4
Normal file
Binary file not shown.
BIN
Tests/CallUITest/Resources/test4.mp4
Normal file
BIN
Tests/CallUITest/Resources/test4.mp4
Normal file
Binary file not shown.
@ -16,6 +16,8 @@ public final class AppDelegate: NSObject, UIApplicationDelegate {
|
|||||||
window.rootViewController = ViewController()
|
window.rootViewController = ViewController()
|
||||||
window.makeKeyAndVisible()
|
window.makeKeyAndVisible()
|
||||||
|
|
||||||
|
application.internalSetStatusBarStyle(.lightContent, animated: false)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,28 @@ import Display
|
|||||||
import CallScreen
|
import CallScreen
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
|
|
||||||
|
private extension UIScreen {
|
||||||
|
private static let cornerRadiusKey: String = {
|
||||||
|
let components = ["Radius", "Corner", "display", "_"]
|
||||||
|
return components.reversed().joined()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var displayCornerRadius: CGFloat {
|
||||||
|
guard let cornerRadius = self.value(forKey: Self.cornerRadiusKey) as? CGFloat else {
|
||||||
|
assertionFailure("Failed to detect screen corner radius")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return cornerRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class ViewController: UIViewController {
|
public final class ViewController: UIViewController {
|
||||||
private var callScreenView: PrivateCallScreen?
|
private var callScreenView: PrivateCallScreen?
|
||||||
private var callState: PrivateCallScreen.State = PrivateCallScreen.State(
|
private var callState: PrivateCallScreen.State = PrivateCallScreen.State(
|
||||||
lifecycleState: .connecting,
|
lifecycleState: .connecting,
|
||||||
name: "Emma Walters",
|
name: "Emma Walters",
|
||||||
|
shortName: "Emma",
|
||||||
avatarImage: UIImage(named: "test"),
|
avatarImage: UIImage(named: "test"),
|
||||||
audioOutput: .internalSpeaker,
|
audioOutput: .internalSpeaker,
|
||||||
isMicrophoneMuted: false,
|
isMicrophoneMuted: false,
|
||||||
@ -17,6 +34,11 @@ public final class ViewController: UIViewController {
|
|||||||
remoteVideo: nil
|
remoteVideo: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private var currentLayout: (size: CGSize, insets: UIEdgeInsets)?
|
||||||
|
private var viewLayoutTransition: Transition?
|
||||||
|
|
||||||
|
private var audioLevelTimer: Foundation.Timer?
|
||||||
|
|
||||||
override public func viewDidLoad() {
|
override public func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
@ -45,7 +67,7 @@ public final class ViewController: UIViewController {
|
|||||||
self.callState.lifecycleState = .active(PrivateCallScreen.State.ActiveState(
|
self.callState.lifecycleState = .active(PrivateCallScreen.State.ActiveState(
|
||||||
startTime: Date().timeIntervalSince1970,
|
startTime: Date().timeIntervalSince1970,
|
||||||
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
|
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
|
||||||
emojiKey: ["A", "B", "C", "D"]
|
emojiKey: ["😂", "😘", "😍", "😊"]
|
||||||
))
|
))
|
||||||
case var .active(activeState):
|
case var .active(activeState):
|
||||||
activeState.signalInfo.quality = activeState.signalInfo.quality == 1.0 ? 0.1 : 1.0
|
activeState.signalInfo.quality = activeState.signalInfo.quality == 1.0 ? 0.1 : 1.0
|
||||||
@ -54,10 +76,33 @@ public final class ViewController: UIViewController {
|
|||||||
self.callState.lifecycleState = .active(PrivateCallScreen.State.ActiveState(
|
self.callState.lifecycleState = .active(PrivateCallScreen.State.ActiveState(
|
||||||
startTime: Date().timeIntervalSince1970,
|
startTime: Date().timeIntervalSince1970,
|
||||||
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
|
signalInfo: PrivateCallScreen.State.SignalInfo(quality: 1.0),
|
||||||
emojiKey: ["A", "B", "C", "D"]
|
emojiKey: ["😂", "😘", "😍", "😊"]
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch self.callState.lifecycleState {
|
||||||
|
case .terminated:
|
||||||
|
if let audioLevelTimer = self.audioLevelTimer {
|
||||||
|
self.audioLevelTimer = nil
|
||||||
|
audioLevelTimer.invalidate()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if self.audioLevelTimer == nil {
|
||||||
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
self.audioLevelTimer = Foundation.Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true, block: { [weak self] _ in
|
||||||
|
guard let self, let callScreenView = self.callScreenView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let timestamp = CFAbsoluteTimeGetCurrent() - startTime
|
||||||
|
let stream1 = sin(timestamp * Double.pi * 2.0)
|
||||||
|
let stream2 = sin(2.0 * timestamp * Double.pi * 2.0)
|
||||||
|
let stream3 = sin(3.0 * timestamp * Double.pi * 2.0)
|
||||||
|
let result = stream1 + stream2 + stream3
|
||||||
|
callScreenView.addIncomingAudioLevel(value: abs(Float(result)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.update(transition: .spring(duration: 0.4))
|
self.update(transition: .spring(duration: 0.4))
|
||||||
}
|
}
|
||||||
callScreenView.flipCameraAction = { [weak self] in
|
callScreenView.flipCameraAction = { [weak self] in
|
||||||
@ -66,6 +111,8 @@ public final class ViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
if let input = self.callState.localVideo as? FileVideoSource {
|
if let input = self.callState.localVideo as? FileVideoSource {
|
||||||
input.sourceId = input.sourceId == 0 ? 1 : 0
|
input.sourceId = input.sourceId == 0 ? 1 : 0
|
||||||
|
input.fixedRotationAngle = input.sourceId == 0 ? Float.pi * 0.0 : Float.pi * 0.5
|
||||||
|
input.sizeMultiplicator = input.sourceId == 0 ? CGPoint(x: 1.0, y: 1.0) : CGPoint(x: 1.5, y: 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
callScreenView.videoAction = { [weak self] in
|
callScreenView.videoAction = { [weak self] in
|
||||||
@ -73,7 +120,7 @@ public final class ViewController: UIViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.callState.localVideo == nil {
|
if self.callState.localVideo == nil {
|
||||||
self.callState.localVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!)
|
self.callState.localVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test3", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 0.0)
|
||||||
} else {
|
} else {
|
||||||
self.callState.localVideo = nil
|
self.callState.localVideo = nil
|
||||||
}
|
}
|
||||||
@ -81,7 +128,7 @@ public final class ViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
callScreenView.microhoneMuteAction = {
|
callScreenView.microhoneMuteAction = {
|
||||||
if self.callState.remoteVideo == nil {
|
if self.callState.remoteVideo == nil {
|
||||||
self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test2", withExtension: "mp4")!)
|
self.callState.remoteVideo = FileVideoSource(device: MetalEngine.shared.device, url: Bundle.main.url(forResource: "test4", withExtension: "mp4")!, fixedRotationAngle: Float.pi * 0.0)
|
||||||
} else {
|
} else {
|
||||||
self.callState.remoteVideo = nil
|
self.callState.remoteVideo = nil
|
||||||
}
|
}
|
||||||
@ -96,32 +143,44 @@ public final class ViewController: UIViewController {
|
|||||||
self.callState.localVideo = nil
|
self.callState.localVideo = nil
|
||||||
self.update(transition: .spring(duration: 0.4))
|
self.update(transition: .spring(duration: 0.4))
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update(transition: .immediate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func update(transition: Transition) {
|
private func update(transition: Transition) {
|
||||||
self.update(size: self.view.bounds.size, transition: transition)
|
if let (size, insets) = self.currentLayout {
|
||||||
|
self.update(size: size, insets: insets, transition: transition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func update(size: CGSize, transition: Transition) {
|
private func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
|
||||||
guard let callScreenView = self.callScreenView else {
|
guard let callScreenView = self.callScreenView else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
transition.setFrame(view: callScreenView, frame: CGRect(origin: CGPoint(), size: size))
|
transition.setFrame(view: callScreenView, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
let insets: UIEdgeInsets
|
callScreenView.update(size: size, insets: insets, screenCornerRadius: UIScreen.main.displayCornerRadius, state: self.callState, transition: transition)
|
||||||
if size.width < size.height {
|
}
|
||||||
insets = UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
|
|
||||||
} else {
|
override public func viewWillLayoutSubviews() {
|
||||||
insets = UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0)
|
super.viewWillLayoutSubviews()
|
||||||
|
|
||||||
|
let safeAreaLayoutGuide = self.view.safeAreaLayoutGuide
|
||||||
|
|
||||||
|
let size = self.view.bounds.size
|
||||||
|
let insets = UIEdgeInsets(top: safeAreaLayoutGuide.layoutFrame.minY, left: safeAreaLayoutGuide.layoutFrame.minX, bottom: size.height - safeAreaLayoutGuide.layoutFrame.maxY, right: safeAreaLayoutGuide.layoutFrame.minX)
|
||||||
|
|
||||||
|
let transition = self.viewLayoutTransition ?? .immediate
|
||||||
|
self.viewLayoutTransition = nil
|
||||||
|
|
||||||
|
if let currentLayout = self.currentLayout, currentLayout == (size, insets) {
|
||||||
|
} else {
|
||||||
|
self.currentLayout = (size, insets)
|
||||||
|
self.update(size: size, insets: insets, transition: transition)
|
||||||
}
|
}
|
||||||
callScreenView.update(size: size, insets: insets, screenCornerRadius: 55.0, state: self.callState, transition: transition)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||||
super.viewWillTransition(to: size, with: coordinator)
|
super.viewWillTransition(to: size, with: coordinator)
|
||||||
|
|
||||||
self.update(size: size, transition: .easeInOut(duration: 0.3))
|
self.viewLayoutTransition = .easeInOut(duration: 0.3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,13 @@ public func chatTextInputAddFormattingAttribute(_ state: ChatTextInputState, att
|
|||||||
state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, _ in
|
state.inputText.enumerateAttributes(in: nsRange, options: .longestEffectiveRangeNotRequired) { attributes, range, _ in
|
||||||
for (key, _) in attributes {
|
for (key, _) in attributes {
|
||||||
if key == attribute {
|
if key == attribute {
|
||||||
|
if nsRange == range {
|
||||||
addAttribute = false
|
addAttribute = false
|
||||||
attributesToRemove.append(key)
|
attributesToRemove.append(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var selectionRange = state.selectionRange
|
var selectionRange = state.selectionRange
|
||||||
|
|
||||||
|
@ -481,8 +481,14 @@ public struct Transition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func setScale(layer: CALayer, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
public func setScale(layer: CALayer, scale: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||||
let t = layer.presentation()?.transform ?? layer.transform
|
let currentTransform: CATransform3D
|
||||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
if layer.animation(forKey: "transform") != nil || layer.animation(forKey: "transform.scale") != nil {
|
||||||
|
currentTransform = layer.presentation()?.transform ?? layer.transform
|
||||||
|
} else {
|
||||||
|
currentTransform = layer.transform
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentScale = sqrt((currentTransform.m11 * currentTransform.m11) + (currentTransform.m12 * currentTransform.m12) + (currentTransform.m13 * currentTransform.m13))
|
||||||
if currentScale == scale {
|
if currentScale == scale {
|
||||||
if let animation = layer.animation(forKey: "transform.scale") as? CABasicAnimation, let toValue = animation.toValue as? NSNumber {
|
if let animation = layer.animation(forKey: "transform.scale") as? CABasicAnimation, let toValue = animation.toValue as? NSNumber {
|
||||||
if toValue.doubleValue == scale {
|
if toValue.doubleValue == scale {
|
||||||
@ -591,7 +597,7 @@ public struct Transition {
|
|||||||
completion?(true)
|
completion?(true)
|
||||||
case let .curve(duration, curve):
|
case let .curve(duration, curve):
|
||||||
let previousValue: CATransform3D
|
let previousValue: CATransform3D
|
||||||
if let presentation = layer.presentation() {
|
if layer.animation(forKey: "transform") != nil, let presentation = layer.presentation() {
|
||||||
previousValue = presentation.transform
|
previousValue = presentation.transform
|
||||||
} else {
|
} else {
|
||||||
previousValue = layer.transform
|
previousValue = layer.transform
|
||||||
@ -704,6 +710,33 @@ public struct Transition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setZPosition(layer: CALayer, zPosition: CGFloat, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
if layer.zPosition == zPosition {
|
||||||
|
completion?(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch self.animation {
|
||||||
|
case .none:
|
||||||
|
layer.zPosition = zPosition
|
||||||
|
layer.removeAnimation(forKey: "zPosition")
|
||||||
|
completion?(true)
|
||||||
|
case let .curve(duration, curve):
|
||||||
|
let previousZPosition = layer.presentation()?.opacity ?? layer.opacity
|
||||||
|
layer.zPosition = zPosition
|
||||||
|
layer.animate(
|
||||||
|
from: previousZPosition as NSNumber,
|
||||||
|
to: zPosition as NSNumber,
|
||||||
|
keyPath: "zPosition",
|
||||||
|
duration: duration,
|
||||||
|
delay: delay,
|
||||||
|
curve: curve,
|
||||||
|
removeOnCompletion: true,
|
||||||
|
additive: false,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, delay: Double = 0.0, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
|
@ -203,9 +203,9 @@ public extension CALayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func animate(from: AnyObject?, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
func animate(from: AnyObject?, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil, key: String? = nil) {
|
||||||
let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||||
self.add(animation, forKey: additive ? nil : keyPath)
|
self.add(animation, forKey: key ?? (additive ? nil : keyPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateGroup(_ animations: [CAAnimation], key: String, completion: ((Bool) -> Void)? = nil) {
|
func animateGroup(_ animations: [CAAnimation], key: String, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
@ -87,3 +87,32 @@ open class SimpleGradientLayer: CAGradientLayer {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
open class SimpleTransformLayer: CATransformLayer {
|
||||||
|
public var didEnterHierarchy: (() -> Void)?
|
||||||
|
public var didExitHierarchy: (() -> Void)?
|
||||||
|
public private(set) var isInHierarchy: Bool = false
|
||||||
|
|
||||||
|
override open func action(forKey event: String) -> CAAction? {
|
||||||
|
if event == kCAOnOrderIn {
|
||||||
|
self.isInHierarchy = true
|
||||||
|
self.didEnterHierarchy?()
|
||||||
|
} else if event == kCAOnOrderOut {
|
||||||
|
self.isInHierarchy = false
|
||||||
|
self.didExitHierarchy?()
|
||||||
|
}
|
||||||
|
return nullAction
|
||||||
|
}
|
||||||
|
|
||||||
|
override public init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override public init(layer: Any) {
|
||||||
|
super.init(layer: layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -115,6 +115,32 @@ open class MetalEngineSubjectLayer: SimpleLayer {
|
|||||||
fileprivate var internalId: Int = -1
|
fileprivate var internalId: Int = -1
|
||||||
fileprivate var surfaceAllocation: MetalEngine.SurfaceAllocation?
|
fileprivate var surfaceAllocation: MetalEngine.SurfaceAllocation?
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
fileprivate var surfaceChangeFrameCount: Int = 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public var cloneLayers: [CALayer] = []
|
||||||
|
|
||||||
|
override open var contents: Any? {
|
||||||
|
didSet {
|
||||||
|
if !self.cloneLayers.isEmpty {
|
||||||
|
for cloneLayer in self.cloneLayers {
|
||||||
|
cloneLayer.contents = self.contents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override open var contentsRect: CGRect {
|
||||||
|
didSet {
|
||||||
|
if !self.cloneLayers.isEmpty {
|
||||||
|
for cloneLayer in self.cloneLayers {
|
||||||
|
cloneLayer.contentsRect = self.contentsRect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override init() {
|
public override init() {
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
@ -529,10 +555,13 @@ public final class MetalEngine {
|
|||||||
let renderingRect: CGRect
|
let renderingRect: CGRect
|
||||||
let contentsRect: CGRect
|
let contentsRect: CGRect
|
||||||
|
|
||||||
init(baseRect: CGRect, surfaceWidth: Int, surfaceHeight: Int) {
|
init(baseRect: CGRect, edgeSize: CGFloat, surfaceWidth: Int, surfaceHeight: Int) {
|
||||||
self.subRect = CGRect(origin: CGPoint(x: baseRect.minX, y: baseRect.minY), size: CGSize(width: baseRect.width, height: baseRect.height))
|
self.subRect = CGRect(origin: CGPoint(x: baseRect.minX, y: baseRect.minY), size: CGSize(width: baseRect.width, height: baseRect.height))
|
||||||
self.renderingRect = CGRect(origin: CGPoint(x: self.subRect.minX / CGFloat(surfaceWidth), y: self.subRect.minY / CGFloat(surfaceHeight)), size: CGSize(width: self.subRect.width / CGFloat(surfaceWidth), height: self.subRect.height / CGFloat(surfaceHeight)))
|
self.renderingRect = CGRect(origin: CGPoint(x: self.subRect.minX / CGFloat(surfaceWidth), y: self.subRect.minY / CGFloat(surfaceHeight)), size: CGSize(width: self.subRect.width / CGFloat(surfaceWidth), height: self.subRect.height / CGFloat(surfaceHeight)))
|
||||||
self.contentsRect = CGRect(origin: CGPoint(x: self.subRect.minX / CGFloat(surfaceWidth), y: 1.0 - self.subRect.minY / CGFloat(surfaceHeight) - self.subRect.height / CGFloat(surfaceHeight)), size: CGSize(width: self.subRect.width / CGFloat(surfaceWidth), height: self.subRect.height / CGFloat(surfaceHeight)))
|
|
||||||
|
let subRectWithInset = self.subRect.insetBy(dx: edgeSize, dy: edgeSize)
|
||||||
|
|
||||||
|
self.contentsRect = CGRect(origin: CGPoint(x: subRectWithInset.minX / CGFloat(surfaceWidth), y: 1.0 - subRectWithInset.minY / CGFloat(surfaceHeight) - subRectWithInset.height / CGFloat(surfaceHeight)), size: CGSize(width: subRectWithInset.width / CGFloat(surfaceWidth), height: subRectWithInset.height / CGFloat(surfaceHeight)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -546,11 +575,13 @@ public final class MetalEngine {
|
|||||||
if item0.itemId != -1 && item1.itemId != -1 {
|
if item0.itemId != -1 && item1.itemId != -1 {
|
||||||
let layout0 = AllocationLayout(
|
let layout0 = AllocationLayout(
|
||||||
baseRect: CGRect(origin: CGPoint(x: CGFloat(item0.x), y: CGFloat(item0.y)), size: CGSize(width: CGFloat(item0.width), height: CGFloat(item0.height))),
|
baseRect: CGRect(origin: CGPoint(x: CGFloat(item0.x), y: CGFloat(item0.y)), size: CGSize(width: CGFloat(item0.width), height: CGFloat(item0.height))),
|
||||||
|
edgeSize: CGFloat(renderingParameters.edgeInset),
|
||||||
surfaceWidth: self.width,
|
surfaceWidth: self.width,
|
||||||
surfaceHeight: self.height
|
surfaceHeight: self.height
|
||||||
)
|
)
|
||||||
let layout1 = AllocationLayout(
|
let layout1 = AllocationLayout(
|
||||||
baseRect: CGRect(origin: CGPoint(x: CGFloat(item1.x), y: CGFloat(item1.y)), size: CGSize(width: CGFloat(item1.width), height: CGFloat(item1.height))),
|
baseRect: CGRect(origin: CGPoint(x: CGFloat(item1.x), y: CGFloat(item1.y)), size: CGSize(width: CGFloat(item1.width), height: CGFloat(item1.height))),
|
||||||
|
edgeSize: CGFloat(renderingParameters.edgeInset),
|
||||||
surfaceWidth: self.width,
|
surfaceWidth: self.width,
|
||||||
surfaceHeight: self.height
|
surfaceHeight: self.height
|
||||||
)
|
)
|
||||||
@ -780,7 +811,10 @@ public final class MetalEngine {
|
|||||||
|
|
||||||
if previousSurfaceId != nil {
|
if previousSurfaceId != nil {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
layer.surfaceChangeFrameCount += 1
|
||||||
|
if layer.surfaceChangeFrameCount > 100 {
|
||||||
print("Changing surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight))")
|
print("Changing surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight))")
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -792,6 +826,10 @@ public final class MetalEngine {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
#if DEBUG
|
||||||
|
layer.surfaceChangeFrameCount = max(0, layer.surfaceChangeFrameCount - 1)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ private func interpolate(from: CGFloat, to: CGFloat, value: CGFloat) -> CGFloat
|
|||||||
return (1.0 - value) * from + value * to
|
return (1.0 - value) * from + value * to
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class CallVideoNode: ASDisplayNode, PreviewVideoNode {
|
final class CallVideoNode: ASDisplayNode, PreviewVideoNode {
|
||||||
private let videoTransformContainer: ASDisplayNode
|
private let videoTransformContainer: ASDisplayNode
|
||||||
private let videoView: PresentationCallVideoView
|
private let videoView: PresentationCallVideoView
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import TinyThumbnail
|
|||||||
import ImageBlur
|
import ImageBlur
|
||||||
import TelegramVoip
|
import TelegramVoip
|
||||||
import MetalEngine
|
import MetalEngine
|
||||||
|
import DeviceAccess
|
||||||
|
|
||||||
final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol {
|
final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeProtocol {
|
||||||
private let sharedContext: SharedAccountContext
|
private let sharedContext: SharedAccountContext
|
||||||
@ -32,6 +33,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||||||
|
|
||||||
private var callStartTimestamp: Double?
|
private var callStartTimestamp: Double?
|
||||||
|
|
||||||
|
private var callState: PresentationCallState?
|
||||||
var isMuted: Bool = false
|
var isMuted: Bool = false
|
||||||
|
|
||||||
var toggleMute: (() -> Void)?
|
var toggleMute: (() -> Void)?
|
||||||
@ -56,6 +58,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||||||
private var isMicrophoneMutedDisposable: Disposable?
|
private var isMicrophoneMutedDisposable: Disposable?
|
||||||
private var audioLevelDisposable: Disposable?
|
private var audioLevelDisposable: Disposable?
|
||||||
|
|
||||||
|
private var localVideo: AdaptedCallVideoSource?
|
||||||
private var remoteVideo: AdaptedCallVideoSource?
|
private var remoteVideo: AdaptedCallVideoSource?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@ -94,7 +97,13 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let _ = self
|
self.toggleVideo()
|
||||||
|
}
|
||||||
|
self.callScreen.flipCameraAction = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.call.switchVideoCamera()
|
||||||
}
|
}
|
||||||
self.callScreen.microhoneMuteAction = { [weak self] in
|
self.callScreen.microhoneMuteAction = { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -112,6 +121,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||||||
self.callScreenState = PrivateCallScreen.State(
|
self.callScreenState = PrivateCallScreen.State(
|
||||||
lifecycleState: .connecting,
|
lifecycleState: .connecting,
|
||||||
name: " ",
|
name: " ",
|
||||||
|
shortName: " ",
|
||||||
avatarImage: nil,
|
avatarImage: nil,
|
||||||
audioOutput: .internalSpeaker,
|
audioOutput: .internalSpeaker,
|
||||||
isMicrophoneMuted: false,
|
isMicrophoneMuted: false,
|
||||||
@ -176,6 +186,93 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func toggleVideo() {
|
||||||
|
guard let callState = self.callState else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch callState.state {
|
||||||
|
case .active:
|
||||||
|
switch callState.videoState {
|
||||||
|
case .active(let isScreencast), .paused(let isScreencast):
|
||||||
|
if isScreencast {
|
||||||
|
(self.call as? PresentationCallImpl)?.disableScreencast()
|
||||||
|
} else {
|
||||||
|
self.call.disableVideo()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
DeviceAccess.authorizeAccess(to: .camera(.videoCall), onlyCheck: true, presentationData: self.presentationData, present: { [weak self] c, a in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.present?(c)
|
||||||
|
}
|
||||||
|
}, openSettings: { [weak self] in
|
||||||
|
self?.sharedContext.applicationBindings.openSettings()
|
||||||
|
}, _: { [weak self] ready in
|
||||||
|
guard let self, ready else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let proceed = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
/*switch callState.videoState {
|
||||||
|
case .inactive:
|
||||||
|
self.isRequestingVideo = true
|
||||||
|
self.updateButtonsMode()
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}*/
|
||||||
|
self.call.requestVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.call.makeOutgoingVideoView(completion: { [weak self] outgoingVideoView in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let outgoingVideoView = outgoingVideoView {
|
||||||
|
outgoingVideoView.view.backgroundColor = .black
|
||||||
|
outgoingVideoView.view.clipsToBounds = true
|
||||||
|
|
||||||
|
var updateLayoutImpl: ((ContainerViewLayout, CGFloat) -> Void)?
|
||||||
|
|
||||||
|
let outgoingVideoNode = CallVideoNode(videoView: outgoingVideoView, disabledText: nil, assumeReadyAfterTimeout: true, isReadyUpdated: { [weak self] in
|
||||||
|
guard let self, let (layout, navigationBarHeight) = self.validLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateLayoutImpl?(layout, navigationBarHeight)
|
||||||
|
}, orientationUpdated: { [weak self] in
|
||||||
|
guard let self, let (layout, navigationBarHeight) = self.validLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateLayoutImpl?(layout, navigationBarHeight)
|
||||||
|
}, isFlippedUpdated: { [weak self] _ in
|
||||||
|
guard let self, let (layout, navigationBarHeight) = self.validLayout else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateLayoutImpl?(layout, navigationBarHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
let controller = VoiceChatCameraPreviewController(sharedContext: self.sharedContext, cameraNode: outgoingVideoNode, shareCamera: { _, _ in
|
||||||
|
proceed()
|
||||||
|
}, switchCamera: { [weak self] in
|
||||||
|
Queue.mainQueue().after(0.1) {
|
||||||
|
self?.call.switchVideoCamera()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.present?(controller)
|
||||||
|
|
||||||
|
updateLayoutImpl = { [weak controller] layout, navigationBarHeight in
|
||||||
|
controller?.containerLayoutUpdated(layout, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func resolvedEmojiKey(data: Data) -> [String] {
|
private func resolvedEmojiKey(data: Data) -> [String] {
|
||||||
if let emojiKey = self.emojiKey, emojiKey.data == data {
|
if let emojiKey = self.emojiKey, emojiKey.data == data {
|
||||||
return emojiKey.resolvedKey
|
return emojiKey.resolvedKey
|
||||||
@ -186,6 +283,8 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateCallState(_ callState: PresentationCallState) {
|
func updateCallState(_ callState: PresentationCallState) {
|
||||||
|
self.callState = callState
|
||||||
|
|
||||||
let mappedLifecycleState: PrivateCallScreen.State.LifecycleState
|
let mappedLifecycleState: PrivateCallScreen.State.LifecycleState
|
||||||
switch callState.state {
|
switch callState.state {
|
||||||
case .waiting:
|
case .waiting:
|
||||||
@ -236,9 +335,23 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||||||
self.remoteVideo = nil
|
self.remoteVideo = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch callState.videoState {
|
||||||
|
case .active(let isScreencast), .paused(let isScreencast):
|
||||||
|
if isScreencast {
|
||||||
|
self.localVideo = nil
|
||||||
|
} else {
|
||||||
|
if self.localVideo == nil, let call = self.call as? PresentationCallImpl, let videoStreamSignal = call.video(isIncoming: false) {
|
||||||
|
self.localVideo = AdaptedCallVideoSource(videoStreamSignal: videoStreamSignal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .inactive, .notAvailable:
|
||||||
|
self.localVideo = nil
|
||||||
|
}
|
||||||
|
|
||||||
if var callScreenState = self.callScreenState {
|
if var callScreenState = self.callScreenState {
|
||||||
callScreenState.lifecycleState = mappedLifecycleState
|
callScreenState.lifecycleState = mappedLifecycleState
|
||||||
callScreenState.remoteVideo = self.remoteVideo
|
callScreenState.remoteVideo = self.remoteVideo
|
||||||
|
callScreenState.localVideo = self.localVideo
|
||||||
|
|
||||||
if self.callScreenState != callScreenState {
|
if self.callScreenState != callScreenState {
|
||||||
self.callScreenState = callScreenState
|
self.callScreenState = callScreenState
|
||||||
@ -380,7 +493,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
|
|||||||
|
|
||||||
private final class AdaptedCallVideoSource: VideoSource {
|
private final class AdaptedCallVideoSource: VideoSource {
|
||||||
private static let queue = Queue(name: "AdaptedCallVideoSource")
|
private static let queue = Queue(name: "AdaptedCallVideoSource")
|
||||||
var updated: (() -> Void)?
|
private var onUpdatedListeners = Bag<() -> Void>()
|
||||||
private(set) var currentOutput: Output?
|
private(set) var currentOutput: Output?
|
||||||
|
|
||||||
private var textureCache: CVMetalTextureCache?
|
private var textureCache: CVMetalTextureCache?
|
||||||
@ -425,7 +538,7 @@ private final class AdaptedCallVideoSource: VideoSource {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output = Output(y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, sourceId: videoFrameData.mirrorHorizontally || videoFrameData.mirrorVertically ? 1 : 0)
|
output = Output(resolution: CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height)), y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, sourceId: videoFrameData.mirrorHorizontally || videoFrameData.mirrorVertically ? 1 : 0)
|
||||||
default:
|
default:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -435,12 +548,27 @@ private final class AdaptedCallVideoSource: VideoSource {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.currentOutput = output
|
self.currentOutput = output
|
||||||
self.updated?()
|
for onUpdated in self.onUpdatedListeners.copyItems() {
|
||||||
|
onUpdated()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addOnUpdated(_ f: @escaping () -> Void) -> Disposable {
|
||||||
|
let index = self.onUpdatedListeners.add(f)
|
||||||
|
|
||||||
|
return ActionDisposable { [weak self] in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.onUpdatedListeners.remove(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
self.videoFrameDisposable?.dispose()
|
self.videoFrameDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
@ -896,7 +896,13 @@ public final class PresentationCallImpl: PresentationCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func video(isIncoming: Bool) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError>? {
|
func video(isIncoming: Bool) -> Signal<OngoingGroupCallContext.VideoFrameData, NoError>? {
|
||||||
|
if isIncoming {
|
||||||
return self.ongoingContext?.video(isIncoming: isIncoming)
|
return self.ongoingContext?.video(isIncoming: isIncoming)
|
||||||
|
} else if let videoCapturer = self.videoCapturer {
|
||||||
|
return videoCapturer.video()
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func makeIncomingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) {
|
public func makeIncomingVideoView(completion: @escaping (PresentationCallVideoView?) -> Void) {
|
||||||
|
@ -232,7 +232,7 @@ vertex BlobVertexOut callBlobVertex(
|
|||||||
fragment half4 callBlobFragment(
|
fragment half4 callBlobFragment(
|
||||||
BlobVertexOut in [[stage_in]]
|
BlobVertexOut in [[stage_in]]
|
||||||
) {
|
) {
|
||||||
half alpha = 0.15;
|
half alpha = 0.35;
|
||||||
return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha);
|
return half4(1.0 * alpha, 1.0 * alpha, 1.0 * alpha, alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,3 +345,28 @@ kernel void gaussianBlurVertical(
|
|||||||
) {
|
) {
|
||||||
gaussianBlur(inTexture, outTexture, float2(0, 1), gid);
|
gaussianBlur(inTexture, outTexture, float2(0, 1), gid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vertex QuadVertexOut edgeTestVertex(
|
||||||
|
const device Rectangle &rect [[ buffer(0) ]],
|
||||||
|
unsigned int vid [[ vertex_id ]]
|
||||||
|
) {
|
||||||
|
float2 quadVertex = quadVertices[vid];
|
||||||
|
|
||||||
|
QuadVertexOut out;
|
||||||
|
|
||||||
|
out.position = float4(rect.origin.x + quadVertex.x * rect.size.x, rect.origin.y + quadVertex.y * rect.size.y, 0.0, 1.0);
|
||||||
|
out.position.x = -1.0 + out.position.x * 2.0;
|
||||||
|
out.position.y = -1.0 + out.position.y * 2.0;
|
||||||
|
|
||||||
|
out.uv = quadVertex;
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment half4 edgeTestFragment(
|
||||||
|
QuadVertexOut in [[stage_in]],
|
||||||
|
const device float4 &colorIn
|
||||||
|
) {
|
||||||
|
half4 color = half4(colorIn);
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
@ -57,13 +57,31 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, buttons: [Button], transition: Transition) {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
guard let result = super.hitTest(point, with: event) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if result === self {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(size: CGSize, insets: UIEdgeInsets, controlsHidden: Bool, buttons: [Button], transition: Transition) -> CGFloat {
|
||||||
self.buttons = buttons
|
self.buttons = buttons
|
||||||
|
|
||||||
let buttonSize: CGFloat = 56.0
|
let buttonSize: CGFloat = 56.0
|
||||||
let buttonSpacing: CGFloat = 36.0
|
let buttonSpacing: CGFloat = 36.0
|
||||||
|
|
||||||
let buttonY: CGFloat = size.height - 86.0 - buttonSize
|
let buttonY: CGFloat
|
||||||
|
let resultHeight: CGFloat
|
||||||
|
if controlsHidden {
|
||||||
|
buttonY = size.height + 12.0
|
||||||
|
resultHeight = insets.bottom + 4.0
|
||||||
|
} else {
|
||||||
|
buttonY = size.height - insets.bottom - 52.0 - buttonSize
|
||||||
|
resultHeight = size.height - buttonY
|
||||||
|
}
|
||||||
var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(buttons.count) - buttonSpacing * CGFloat(buttons.count - 1)) * 0.5)
|
var buttonX: CGFloat = floor((size.width - buttonSize * CGFloat(buttons.count) - buttonSpacing * CGFloat(buttons.count - 1)) * 0.5)
|
||||||
|
|
||||||
for button in buttons {
|
for button in buttons {
|
||||||
@ -137,5 +155,7 @@ final class ButtonGroupView: OverlayMaskContainerView {
|
|||||||
for key in removeKeys {
|
for key in removeKeys {
|
||||||
self.buttonViews.removeValue(forKey: key)
|
self.buttonViews.removeValue(forKey: key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resultHeight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,7 @@ final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
]
|
]
|
||||||
|
|
||||||
let blurredLayer: MetalEngineSubjectLayer
|
let blurredLayer: MetalEngineSubjectLayer
|
||||||
|
let externalBlurredLayer: MetalEngineSubjectLayer
|
||||||
|
|
||||||
private var phase: Float = 0.0
|
private var phase: Float = 0.0
|
||||||
|
|
||||||
@ -100,6 +101,7 @@ final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.blurredLayer = MetalEngineSubjectLayer()
|
self.blurredLayer = MetalEngineSubjectLayer()
|
||||||
|
self.externalBlurredLayer = MetalEngineSubjectLayer()
|
||||||
|
|
||||||
self.colorSets = [
|
self.colorSets = [
|
||||||
ColorSet(colors: [
|
ColorSet(colors: [
|
||||||
@ -125,6 +127,8 @@ final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
self.blurredLayer.cloneLayers.append(self.externalBlurredLayer)
|
||||||
|
|
||||||
self.didEnterHierarchy = { [weak self] in
|
self.didEnterHierarchy = { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -154,6 +158,7 @@ final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
|
|
||||||
override init(layer: Any) {
|
override init(layer: Any) {
|
||||||
self.blurredLayer = MetalEngineSubjectLayer()
|
self.blurredLayer = MetalEngineSubjectLayer()
|
||||||
|
self.externalBlurredLayer = MetalEngineSubjectLayer()
|
||||||
self.colorSets = []
|
self.colorSets = []
|
||||||
self.colorTransition = AnimatedProperty<ColorSet>(ColorSet(colors: []))
|
self.colorTransition = AnimatedProperty<ColorSet>(ColorSet(colors: []))
|
||||||
|
|
||||||
@ -187,7 +192,8 @@ final class CallBackgroundLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
for i in 0 ..< 2 {
|
for i in 0 ..< 2 {
|
||||||
let isBlur = i == 1
|
let isBlur = i == 1
|
||||||
context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: i == 0 ? self : self.blurredLayer, commands: { encoder, placement in
|
context.renderToLayer(spec: renderSpec, state: RenderState.self, layer: i == 0 ? self : self.blurredLayer, commands: { encoder, placement in
|
||||||
let effectiveRect = placement.effectiveRect
|
var effectiveRect = placement.effectiveRect
|
||||||
|
effectiveRect = effectiveRect.insetBy(dx: -effectiveRect.width * 0.1, dy: -effectiveRect.height * 0.1)
|
||||||
|
|
||||||
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
|
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width), Float(effectiveRect.height))
|
||||||
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
|
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
|
||||||
|
@ -36,18 +36,6 @@ final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class RenderState: RenderToLayerState {
|
final class RenderState: RenderToLayerState {
|
||||||
final class Input {
|
|
||||||
let rect: CGRect
|
|
||||||
let blobs: [Blob]
|
|
||||||
let phase: Float
|
|
||||||
|
|
||||||
init(rect: CGRect, blobs: [Blob], phase: Float) {
|
|
||||||
self.rect = rect
|
|
||||||
self.blobs = blobs
|
|
||||||
self.phase = phase
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let pipelineState: MTLRenderPipelineState
|
let pipelineState: MTLRenderPipelineState
|
||||||
|
|
||||||
required init?(device: MTLDevice) {
|
required init?(device: MTLDevice) {
|
||||||
@ -133,7 +121,7 @@ final class CallBlobsLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
|||||||
let phase = self.phase
|
let phase = self.phase
|
||||||
let blobs = self.blobs
|
let blobs = self.blobs
|
||||||
|
|
||||||
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(self.bounds.width * 3.0), height: Int(self.bounds.height * 3.0))), state: RenderState.self, layer: self, commands: { encoder, placement in
|
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: Int(self.bounds.width * 3.0), height: Int(self.bounds.height * 3.0)), edgeInset: 4), state: RenderState.self, layer: self, commands: { encoder, placement in
|
||||||
let rect = placement.effectiveRect
|
let rect = placement.effectiveRect
|
||||||
|
|
||||||
for i in 0 ..< blobs.count {
|
for i in 0 ..< blobs.count {
|
||||||
|
@ -64,7 +64,7 @@ final class ContentOverlayButton: HighlightTrackingButton, OverlayMaskContainerV
|
|||||||
|
|
||||||
if highlighted {
|
if highlighted {
|
||||||
self.layer.removeAnimation(forKey: "opacity")
|
self.layer.removeAnimation(forKey: "opacity")
|
||||||
self.layer.removeAnimation(forKey: "sublayerTransform")
|
self.layer.removeAnimation(forKey: "transform")
|
||||||
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
|
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
|
||||||
transition.setScale(layer: self.layer, scale: topScale)
|
transition.setScale(layer: self.layer, scale: topScale)
|
||||||
} else {
|
} else {
|
||||||
|
@ -0,0 +1,175 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
|
||||||
|
final class EmojiExpandedInfoView: OverlayMaskContainerView {
|
||||||
|
private struct Params: Equatable {
|
||||||
|
var constrainedWidth: CGFloat
|
||||||
|
|
||||||
|
init(constrainedWidth: CGFloat) {
|
||||||
|
self.constrainedWidth = constrainedWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Layout: Equatable {
|
||||||
|
var params: Params
|
||||||
|
var size: CGSize
|
||||||
|
|
||||||
|
init(params: Params, size: CGSize) {
|
||||||
|
self.params = params
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let title: String
|
||||||
|
private let text: String
|
||||||
|
|
||||||
|
private let backgroundView: UIImageView
|
||||||
|
private let separatorLayer: SimpleLayer
|
||||||
|
|
||||||
|
private let titleView: TextView
|
||||||
|
private let textView: TextView
|
||||||
|
|
||||||
|
private let actionButton: HighlightTrackingButton
|
||||||
|
private let actionTitleView: TextView
|
||||||
|
|
||||||
|
private var currentLayout: Layout?
|
||||||
|
|
||||||
|
var closeAction: (() -> Void)?
|
||||||
|
|
||||||
|
init(title: String, text: String) {
|
||||||
|
self.title = title
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
self.backgroundView = UIImageView()
|
||||||
|
let cornerRadius: CGFloat = 18.0
|
||||||
|
let buttonHeight: CGFloat = 56.0
|
||||||
|
self.backgroundView.image = generateImage(CGSize(width: cornerRadius * 2.0 + 10.0, height: cornerRadius + 10.0 + buttonHeight), rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: cornerRadius).cgPath)
|
||||||
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
|
context.fillPath()
|
||||||
|
|
||||||
|
context.setBlendMode(.copy)
|
||||||
|
context.setFillColor(UIColor.clear.cgColor)
|
||||||
|
context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height - buttonHeight), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||||
|
})?.stretchableImage(withLeftCapWidth: Int(cornerRadius) + 5, topCapHeight: Int(cornerRadius) + 5)
|
||||||
|
|
||||||
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.titleView = TextView()
|
||||||
|
self.textView = TextView()
|
||||||
|
|
||||||
|
self.actionButton = HighlightTrackingButton()
|
||||||
|
self.actionTitleView = TextView()
|
||||||
|
self.actionTitleView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
|
self.maskContents.addSubview(self.backgroundView)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
|
|
||||||
|
self.addSubview(self.titleView)
|
||||||
|
self.addSubview(self.textView)
|
||||||
|
|
||||||
|
self.addSubview(self.actionButton)
|
||||||
|
self.actionButton.addSubview(self.actionTitleView)
|
||||||
|
|
||||||
|
self.actionButton.internalHighligthedChanged = { [weak self] highlighted in
|
||||||
|
if let self, self.bounds.width > 0.0 {
|
||||||
|
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
|
||||||
|
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
|
||||||
|
|
||||||
|
if highlighted {
|
||||||
|
self.actionButton.layer.removeAnimation(forKey: "sublayerTransform")
|
||||||
|
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
|
||||||
|
transition.setScale(layer: self.actionButton.layer, scale: topScale)
|
||||||
|
} else {
|
||||||
|
let t = self.actionButton.layer.presentation()?.transform ?? layer.transform
|
||||||
|
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||||
|
|
||||||
|
let transition = Transition(animation: .none)
|
||||||
|
transition.setScale(layer: self.actionButton.layer, scale: 1.0)
|
||||||
|
|
||||||
|
self.actionButton.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in
|
||||||
|
guard let self, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.actionButton.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.actionButton.addTarget(self, action: #selector(self.actionButtonPressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func actionButtonPressed() {
|
||||||
|
self.closeAction?()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
self.closeAction?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
if let result = self.actionButton.hitTest(self.convert(point, to: self.actionButton), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(constrainedWidth: CGFloat, transition: Transition) -> CGSize {
|
||||||
|
let params = Params(constrainedWidth: constrainedWidth)
|
||||||
|
if let currentLayout = self.currentLayout, currentLayout.params == params {
|
||||||
|
return currentLayout.size
|
||||||
|
}
|
||||||
|
let size = self.update(params: params, transition: transition)
|
||||||
|
self.currentLayout = Layout(params: params, size: size)
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(params: Params, transition: Transition) -> CGSize {
|
||||||
|
let buttonHeight: CGFloat = 56.0
|
||||||
|
|
||||||
|
var constrainedWidth = params.constrainedWidth
|
||||||
|
constrainedWidth = min(constrainedWidth, 300.0)
|
||||||
|
|
||||||
|
let titleSize = self.titleView.update(string: self.title, fontSize: 16.0, fontWeight: 0.3, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition)
|
||||||
|
let textSize = self.textView.update(string: self.text, fontSize: 16.0, fontWeight: 0.0, alignment: .center, color: .white, constrainedWidth: constrainedWidth - 16.0 * 2.0, transition: transition)
|
||||||
|
|
||||||
|
let contentWidth: CGFloat = max(titleSize.width, textSize.width) + 26.0 * 2.0
|
||||||
|
let contentHeight = 78.0 + titleSize.height + 10.0 + textSize.height + 22.0 + buttonHeight
|
||||||
|
|
||||||
|
let size = CGSize(width: contentWidth, height: contentHeight)
|
||||||
|
|
||||||
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) * 0.5), y: 78.0), size: titleSize)
|
||||||
|
transition.setFrame(view: self.titleView, frame: titleFrame)
|
||||||
|
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) * 0.5), y: titleFrame.maxY + 10.0), size: textSize)
|
||||||
|
transition.setFrame(view: self.textView, frame: textFrame)
|
||||||
|
|
||||||
|
let buttonFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - buttonHeight), size: CGSize(width: size.width, height: buttonHeight))
|
||||||
|
transition.setFrame(view: self.actionButton, frame: buttonFrame)
|
||||||
|
|
||||||
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: size.height - buttonHeight), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||||
|
|
||||||
|
let actionTitleSize = self.actionTitleView.update(string: "OK", fontSize: 19.0, fontWeight: 0.3, color: .white, constrainedWidth: size.width, transition: transition)
|
||||||
|
let actionTitleFrame = CGRect(origin: CGPoint(x: floor((buttonFrame.width - actionTitleSize.width) * 0.5), y: floor((buttonFrame.height - actionTitleSize.height) * 0.5)), size: actionTitleSize)
|
||||||
|
transition.setFrame(view: self.actionTitleView, frame: actionTitleFrame)
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
|
||||||
|
final class EmojiTooltipView: UIView {
|
||||||
|
let size: CGSize
|
||||||
|
|
||||||
|
init(text: String) {
|
||||||
|
self.size = CGSize()
|
||||||
|
|
||||||
|
super.init(frame: CGRect())
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
@ -1,47 +1,94 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
import Display
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
|
||||||
final class KeyEmojiView: UIView {
|
final class KeyEmojiView: HighlightTrackingButton {
|
||||||
|
private struct Params: Equatable {
|
||||||
|
var isExpanded: Bool
|
||||||
|
|
||||||
|
init(isExpanded: Bool) {
|
||||||
|
self.isExpanded = isExpanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Layout: Equatable {
|
||||||
|
var params: Params
|
||||||
|
var size: CGSize
|
||||||
|
|
||||||
|
init(params: Params, size: CGSize) {
|
||||||
|
self.params = params
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let emoji: [String]
|
||||||
private let emojiViews: [TextView]
|
private let emojiViews: [TextView]
|
||||||
|
|
||||||
let size: CGSize
|
var pressAction: (() -> Void)?
|
||||||
|
|
||||||
|
private var currentLayout: Layout?
|
||||||
|
|
||||||
|
var isExpanded: Bool? {
|
||||||
|
return self.currentLayout?.params.isExpanded
|
||||||
|
}
|
||||||
|
|
||||||
init(emoji: [String]) {
|
init(emoji: [String]) {
|
||||||
self.emojiViews = emoji.map { emoji in
|
self.emoji = emoji
|
||||||
|
self.emojiViews = emoji.map { _ in
|
||||||
TextView()
|
TextView()
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemSpacing: CGFloat = 3.0
|
|
||||||
|
|
||||||
var height: CGFloat = 0.0
|
|
||||||
var nextX = 0.0
|
|
||||||
for i in 0 ..< self.emojiViews.count {
|
|
||||||
if nextX != 0.0 {
|
|
||||||
nextX += itemSpacing
|
|
||||||
}
|
|
||||||
let emojiView = self.emojiViews[i]
|
|
||||||
let itemSize = emojiView.update(string: emoji[i], fontSize: 16.0, fontWeight: 0.0, color: .white, constrainedWidth: 100.0, transition: .immediate)
|
|
||||||
if height == 0.0 {
|
|
||||||
height = itemSize.height
|
|
||||||
}
|
|
||||||
emojiView.frame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize)
|
|
||||||
nextX += itemSize.width
|
|
||||||
}
|
|
||||||
|
|
||||||
self.size = CGSize(width: nextX, height: height)
|
|
||||||
|
|
||||||
super.init(frame: CGRect())
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
for emojiView in self.emojiViews {
|
for emojiView in self.emojiViews {
|
||||||
|
emojiView.contentMode = .scaleToFill
|
||||||
|
emojiView.isUserInteractionEnabled = false
|
||||||
self.addSubview(emojiView)
|
self.addSubview(emojiView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.internalHighligthedChanged = { [weak self] highlighted in
|
||||||
|
if let self, self.bounds.width > 0.0 {
|
||||||
|
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
|
||||||
|
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
|
||||||
|
|
||||||
|
if highlighted {
|
||||||
|
self.layer.removeAnimation(forKey: "opacity")
|
||||||
|
self.layer.removeAnimation(forKey: "transform")
|
||||||
|
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
|
||||||
|
transition.setScale(layer: self.layer, scale: topScale)
|
||||||
|
} else {
|
||||||
|
let t = self.layer.presentation()?.transform ?? layer.transform
|
||||||
|
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||||
|
|
||||||
|
let transition = Transition(animation: .none)
|
||||||
|
transition.setScale(layer: self.layer, scale: 1.0)
|
||||||
|
|
||||||
|
self.layer.animateScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in
|
||||||
|
guard let self, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
return super.hitTest(point, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
self.pressAction?()
|
||||||
|
}
|
||||||
|
|
||||||
func animateIn() {
|
func animateIn() {
|
||||||
for i in 0 ..< self.emojiViews.count {
|
for i in 0 ..< self.emojiViews.count {
|
||||||
let emojiView = self.emojiViews[i]
|
let emojiView = self.emojiViews[i]
|
||||||
@ -49,4 +96,84 @@ final class KeyEmojiView: UIView {
|
|||||||
emojiView.layer.animatePosition(from: CGPoint(x: -CGFloat(self.emojiViews.count - 1 - i) * 30.0, y: 0.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
emojiView.layer.animatePosition(from: CGPoint(x: -CGFloat(self.emojiViews.count - 1 - i) * 30.0, y: 0.0), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func update(isExpanded: Bool, transition: Transition) -> CGSize {
|
||||||
|
let params = Params(isExpanded: isExpanded)
|
||||||
|
if let currentLayout = self.currentLayout, currentLayout.params == params {
|
||||||
|
return currentLayout.size
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = self.update(params: params, transition: transition)
|
||||||
|
self.currentLayout = Layout(params: params, size: size)
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(params: Params, transition: Transition) -> CGSize {
|
||||||
|
let itemSpacing: CGFloat = 3.0
|
||||||
|
|
||||||
|
var height: CGFloat = 0.0
|
||||||
|
var nextX = 0.0
|
||||||
|
for i in 0 ..< self.emojiViews.count {
|
||||||
|
if nextX != 0.0 {
|
||||||
|
nextX += itemSpacing
|
||||||
|
}
|
||||||
|
let emojiView = self.emojiViews[i]
|
||||||
|
let itemSize = emojiView.update(string: emoji[i], fontSize: params.isExpanded ? 40.0 : 16.0, fontWeight: 0.0, color: .white, constrainedWidth: 100.0, transition: transition)
|
||||||
|
if height == 0.0 {
|
||||||
|
height = itemSize.height
|
||||||
|
}
|
||||||
|
let itemFrame = CGRect(origin: CGPoint(x: nextX, y: 0.0), size: itemSize)
|
||||||
|
transition.setFrame(view: emojiView, frame: itemFrame)
|
||||||
|
nextX += itemSize.width
|
||||||
|
}
|
||||||
|
|
||||||
|
return CGSize(width: nextX, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateParabollicMotionKeyframes(from sourcePoint: CGPoint, to targetPosition: CGPoint, elevation: CGFloat, duration: Double, curve: Transition.Animation.Curve, reverse: Bool) -> [CGPoint] {
|
||||||
|
let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - elevation)
|
||||||
|
|
||||||
|
let x1 = sourcePoint.x
|
||||||
|
let y1 = sourcePoint.y
|
||||||
|
let x2 = midPoint.x
|
||||||
|
let y2 = midPoint.y
|
||||||
|
let x3 = targetPosition.x
|
||||||
|
let y3 = targetPosition.y
|
||||||
|
|
||||||
|
let numPoints: Int = Int(ceil(Double(UIScreen.main.maximumFramesPerSecond) * duration))
|
||||||
|
|
||||||
|
var keyframes: [CGPoint] = []
|
||||||
|
if abs(y1 - y3) < 5.0 && abs(x1 - x3) < 5.0 {
|
||||||
|
for rawI in 0 ..< numPoints {
|
||||||
|
let i = reverse ? (numPoints - 1 - rawI) : rawI
|
||||||
|
let ks = CGFloat(i) / CGFloat(numPoints - 1)
|
||||||
|
var k = curve.solve(at: reverse ? (1.0 - ks) : ks)
|
||||||
|
if reverse {
|
||||||
|
k = 1.0 - k
|
||||||
|
}
|
||||||
|
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
|
||||||
|
let y = sourcePoint.y * (1.0 - k) + targetPosition.y * k
|
||||||
|
keyframes.append(CGPoint(x: x, y: y))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||||
|
let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||||
|
let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3))
|
||||||
|
|
||||||
|
for rawI in 0 ..< numPoints {
|
||||||
|
let i = reverse ? (numPoints - 1 - rawI) : rawI
|
||||||
|
|
||||||
|
let ks = CGFloat(i) / CGFloat(numPoints - 1)
|
||||||
|
var k = curve.solve(at: reverse ? (1.0 - ks) : ks)
|
||||||
|
if reverse {
|
||||||
|
k = 1.0 - k
|
||||||
|
}
|
||||||
|
let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k
|
||||||
|
let y = a * x * x + b * x + c
|
||||||
|
keyframes.append(CGPoint(x: x, y: y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyframes
|
||||||
}
|
}
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import Display
|
|
||||||
import MetalEngine
|
|
||||||
import ComponentFlow
|
|
||||||
|
|
||||||
final class MinimizedVideoContainerView: UIView {
|
|
||||||
private struct Params: Equatable {
|
|
||||||
var size: CGSize
|
|
||||||
var insets: UIEdgeInsets
|
|
||||||
|
|
||||||
init(size: CGSize, insets: UIEdgeInsets) {
|
|
||||||
self.size = size
|
|
||||||
self.insets = insets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct VideoMetrics: Equatable {
|
|
||||||
var resolution: CGSize
|
|
||||||
var rotationAngle: Float
|
|
||||||
var sourceId: Int
|
|
||||||
|
|
||||||
init(resolution: CGSize, rotationAngle: Float, sourceId: Int) {
|
|
||||||
self.resolution = resolution
|
|
||||||
self.rotationAngle = rotationAngle
|
|
||||||
self.sourceId = sourceId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let videoLayer: PrivateCallVideoLayer
|
|
||||||
|
|
||||||
private var params: Params?
|
|
||||||
private var videoMetrics: VideoMetrics?
|
|
||||||
private var appliedVideoMetrics: VideoMetrics?
|
|
||||||
|
|
||||||
var video: VideoSource? {
|
|
||||||
didSet {
|
|
||||||
self.video?.updated = { [weak self] in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var videoMetrics: VideoMetrics?
|
|
||||||
if let currentOutput = self.video?.currentOutput {
|
|
||||||
self.videoLayer.video = currentOutput
|
|
||||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
|
|
||||||
} else {
|
|
||||||
self.videoLayer.video = nil
|
|
||||||
}
|
|
||||||
self.videoLayer.setNeedsUpdate()
|
|
||||||
|
|
||||||
if self.videoMetrics != videoMetrics {
|
|
||||||
self.videoMetrics = videoMetrics
|
|
||||||
self.update(transition: .easeInOut(duration: 0.2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var videoMetrics: VideoMetrics?
|
|
||||||
if let currentOutput = self.video?.currentOutput {
|
|
||||||
self.videoLayer.video = currentOutput
|
|
||||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
|
|
||||||
} else {
|
|
||||||
self.videoLayer.video = nil
|
|
||||||
}
|
|
||||||
self.videoLayer.setNeedsUpdate()
|
|
||||||
|
|
||||||
if self.videoMetrics != videoMetrics {
|
|
||||||
self.videoMetrics = videoMetrics
|
|
||||||
self.update(transition: .easeInOut(duration: 0.2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
self.videoLayer = PrivateCallVideoLayer()
|
|
||||||
self.videoLayer.masksToBounds = true
|
|
||||||
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
self.layer.addSublayer(self.videoLayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func update(transition: Transition) {
|
|
||||||
guard let params = self.params else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.update(params: params, transition: transition)
|
|
||||||
}
|
|
||||||
|
|
||||||
func update(size: CGSize, insets: UIEdgeInsets, transition: Transition) {
|
|
||||||
let params = Params(size: size, insets: insets)
|
|
||||||
if self.params == params {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.params = params
|
|
||||||
|
|
||||||
self.update(params: params, transition: transition)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func update(params: Params, transition: Transition) {
|
|
||||||
guard let videoMetrics = self.videoMetrics else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var transition = transition
|
|
||||||
if self.appliedVideoMetrics == nil {
|
|
||||||
transition = .immediate
|
|
||||||
}
|
|
||||||
self.appliedVideoMetrics = videoMetrics
|
|
||||||
|
|
||||||
var rotatedResolution = videoMetrics.resolution
|
|
||||||
var videoIsRotated = false
|
|
||||||
if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 {
|
|
||||||
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
|
|
||||||
videoIsRotated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let videoSize = rotatedResolution.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
|
||||||
|
|
||||||
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
|
|
||||||
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
|
|
||||||
|
|
||||||
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
|
||||||
let rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize)
|
|
||||||
|
|
||||||
transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center)
|
|
||||||
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
|
|
||||||
transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
|
|
||||||
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
|
|
||||||
|
|
||||||
transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
|
||||||
transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
|
||||||
|
|
||||||
transition.setCornerRadius(layer: self.videoLayer, cornerRadius: 10.0)
|
|
||||||
|
|
||||||
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,63 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
|
||||||
|
final class RoundedCornersView: UIImageView {
|
||||||
|
private let color: UIColor
|
||||||
|
private var currentCornerRadius: CGFloat?
|
||||||
|
private var cornerImage: UIImage?
|
||||||
|
|
||||||
|
init(color: UIColor) {
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
super.init(image: nil)
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
self.layer.cornerCurve = .circular
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyStaticCornerRadius() {
|
||||||
|
guard let cornerRadius = self.currentCornerRadius else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let cornerImage = self.cornerImage, cornerImage.size.height == cornerRadius * 2.0 {
|
||||||
|
} else {
|
||||||
|
let size = CGSize(width: cornerRadius * 2.0, height: cornerRadius * 2.0)
|
||||||
|
self.cornerImage = generateStretchableFilledCircleImage(diameter: size.width, color: self.color)
|
||||||
|
}
|
||||||
|
self.image = self.cornerImage
|
||||||
|
self.clipsToBounds = false
|
||||||
|
self.backgroundColor = nil
|
||||||
|
self.layer.cornerRadius = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(cornerRadius: CGFloat, transition: Transition) {
|
||||||
|
if self.currentCornerRadius == cornerRadius {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let previousCornerRadius = self.currentCornerRadius
|
||||||
|
self.currentCornerRadius = cornerRadius
|
||||||
|
if transition.animation.isImmediate {
|
||||||
|
self.applyStaticCornerRadius()
|
||||||
|
} else {
|
||||||
|
self.image = nil
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.backgroundColor = self.color
|
||||||
|
if let previousCornerRadius, self.layer.animation(forKey: "cornerRadius") == nil {
|
||||||
|
self.layer.cornerRadius = previousCornerRadius
|
||||||
|
}
|
||||||
|
transition.setCornerRadius(layer: self.layer, cornerRadius: cornerRadius, completion: { [weak self] completed in
|
||||||
|
guard let self, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.applyStaticCornerRadius()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ final class TextView: UIView {
|
|||||||
var fontSize: CGFloat
|
var fontSize: CGFloat
|
||||||
var fontWeight: CGFloat
|
var fontWeight: CGFloat
|
||||||
var monospacedDigits: Bool
|
var monospacedDigits: Bool
|
||||||
|
var alignment: NSTextAlignment
|
||||||
var constrainedWidth: CGFloat
|
var constrainedWidth: CGFloat
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +26,6 @@ final class TextView: UIView {
|
|||||||
|
|
||||||
self.isOpaque = false
|
self.isOpaque = false
|
||||||
self.backgroundColor = nil
|
self.backgroundColor = nil
|
||||||
self.contentMode = .center
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -43,8 +43,8 @@ final class TextView: UIView {
|
|||||||
return super.action(for: layer, forKey: event)
|
return super.action(for: layer, forKey: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(string: String, fontSize: CGFloat, fontWeight: CGFloat, monospacedDigits: Bool = false, color: UIColor, constrainedWidth: CGFloat, transition: Transition) -> CGSize {
|
func update(string: String, fontSize: CGFloat, fontWeight: CGFloat, monospacedDigits: Bool = false, alignment: NSTextAlignment = .natural, color: UIColor, constrainedWidth: CGFloat, transition: Transition) -> CGSize {
|
||||||
let params = Params(string: string, fontSize: fontSize, fontWeight: fontWeight, monospacedDigits: monospacedDigits, constrainedWidth: constrainedWidth)
|
let params = Params(string: string, fontSize: fontSize, fontWeight: fontWeight, monospacedDigits: monospacedDigits, alignment: alignment, constrainedWidth: constrainedWidth)
|
||||||
if let layoutState = self.layoutState, layoutState.params == params {
|
if let layoutState = self.layoutState, layoutState.params == params {
|
||||||
return layoutState.size
|
return layoutState.size
|
||||||
}
|
}
|
||||||
@ -56,9 +56,13 @@ final class TextView: UIView {
|
|||||||
font = UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight(fontWeight))
|
font = UIFont.systemFont(ofSize: fontSize, weight: UIFont.Weight(fontWeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let paragraphStyle = NSMutableParagraphStyle()
|
||||||
|
paragraphStyle.alignment = alignment
|
||||||
|
paragraphStyle.lineSpacing = 0.6
|
||||||
let attributedString = NSAttributedString(string: string, attributes: [
|
let attributedString = NSAttributedString(string: string, attributes: [
|
||||||
.font: font,
|
.font: font,
|
||||||
.foregroundColor: color,
|
.foregroundColor: color,
|
||||||
|
.paragraphStyle: paragraphStyle
|
||||||
])
|
])
|
||||||
let stringBounds = attributedString.boundingRect(with: CGSize(width: constrainedWidth, height: 200.0), options: .usesLineFragmentOrigin, context: nil)
|
let stringBounds = attributedString.boundingRect(with: CGSize(width: constrainedWidth, height: 200.0), options: .usesLineFragmentOrigin, context: nil)
|
||||||
let stringSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
|
let stringSize = CGSize(width: ceil(stringBounds.width), height: ceil(stringBounds.height))
|
||||||
|
@ -3,144 +3,522 @@ import UIKit
|
|||||||
import Display
|
import Display
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import MetalEngine
|
import MetalEngine
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
private let shadowImage: UIImage? = {
|
private let shadowImage: UIImage? = {
|
||||||
UIImage(named: "Call/VideoGradient")?.precomposed()
|
UIImage(named: "Call/VideoGradient")?.precomposed()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
final class VideoContainerView: UIView {
|
private final class VideoContainerLayer: SimpleLayer {
|
||||||
private struct Params: Equatable {
|
let contentsLayer: SimpleLayer
|
||||||
var size: CGSize
|
|
||||||
var insets: UIEdgeInsets
|
|
||||||
var cornerRadius: CGFloat
|
|
||||||
var isMinimized: Bool
|
|
||||||
var isAnimatingOut: Bool
|
|
||||||
|
|
||||||
init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, isMinimized: Bool, isAnimatingOut: Bool) {
|
override init() {
|
||||||
self.size = size
|
self.contentsLayer = SimpleLayer()
|
||||||
self.insets = insets
|
|
||||||
self.cornerRadius = cornerRadius
|
super.init()
|
||||||
self.isMinimized = isMinimized
|
|
||||||
self.isAnimatingOut = isAnimatingOut
|
self.addSublayer(self.contentsLayer)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct VideoMetrics: Equatable {
|
override init(layer: Any) {
|
||||||
var resolution: CGSize
|
self.contentsLayer = SimpleLayer()
|
||||||
var rotationAngle: Float
|
|
||||||
|
|
||||||
init(resolution: CGSize, rotationAngle: Float) {
|
super.init(layer: layer)
|
||||||
self.resolution = resolution
|
|
||||||
self.rotationAngle = rotationAngle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let videoLayer: PrivateCallVideoLayer
|
|
||||||
let blurredContainerLayer: SimpleLayer
|
|
||||||
|
|
||||||
private let topShadowView: UIImageView
|
|
||||||
private let bottomShadowView: UIImageView
|
|
||||||
|
|
||||||
private var params: Params?
|
|
||||||
private var videoMetrics: VideoMetrics?
|
|
||||||
private var appliedVideoMetrics: VideoMetrics?
|
|
||||||
|
|
||||||
var video: VideoSource? {
|
|
||||||
didSet {
|
|
||||||
self.video?.updated = { [weak self] in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var videoMetrics: VideoMetrics?
|
|
||||||
if let currentOutput = self.video?.currentOutput {
|
|
||||||
self.videoLayer.video = currentOutput
|
|
||||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle)
|
|
||||||
} else {
|
|
||||||
self.videoLayer.video = nil
|
|
||||||
}
|
|
||||||
self.videoLayer.setNeedsUpdate()
|
|
||||||
|
|
||||||
if self.videoMetrics != videoMetrics {
|
|
||||||
self.videoMetrics = videoMetrics
|
|
||||||
self.update(transition: .easeInOut(duration: 0.2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var videoMetrics: VideoMetrics?
|
|
||||||
if let currentOutput = self.video?.currentOutput {
|
|
||||||
self.videoLayer.video = currentOutput
|
|
||||||
videoMetrics = VideoMetrics(resolution: CGSize(width: CGFloat(currentOutput.y.width), height: CGFloat(currentOutput.y.height)), rotationAngle: currentOutput.rotationAngle)
|
|
||||||
} else {
|
|
||||||
self.videoLayer.video = nil
|
|
||||||
}
|
|
||||||
self.videoLayer.setNeedsUpdate()
|
|
||||||
|
|
||||||
if self.videoMetrics != videoMetrics {
|
|
||||||
self.videoMetrics = videoMetrics
|
|
||||||
self.update(transition: .easeInOut(duration: 0.2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
self.videoLayer = PrivateCallVideoLayer()
|
|
||||||
self.blurredContainerLayer = SimpleLayer()
|
|
||||||
|
|
||||||
self.topShadowView = UIImageView()
|
|
||||||
self.topShadowView.transform = CGAffineTransformMakeScale(1.0, -1.0)
|
|
||||||
self.bottomShadowView = UIImageView()
|
|
||||||
|
|
||||||
super.init(frame: frame)
|
|
||||||
|
|
||||||
self.backgroundColor = UIColor.black
|
|
||||||
self.blurredContainerLayer.backgroundColor = UIColor.black.cgColor
|
|
||||||
|
|
||||||
self.layer.addSublayer(self.videoLayer)
|
|
||||||
self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer)
|
|
||||||
|
|
||||||
self.topShadowView.image = shadowImage
|
|
||||||
self.bottomShadowView.image = shadowImage
|
|
||||||
self.addSubview(self.topShadowView)
|
|
||||||
self.addSubview(self.bottomShadowView)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func update(size: CGSize, transition: Transition) {
|
||||||
|
transition.setFrame(layer: self.contentsLayer, frame: CGRect(origin: CGPoint(), size: size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class VideoContainerView: HighlightTrackingButton {
|
||||||
|
enum Key {
|
||||||
|
case background
|
||||||
|
case foreground
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Params: Equatable {
|
||||||
|
var size: CGSize
|
||||||
|
var insets: UIEdgeInsets
|
||||||
|
var cornerRadius: CGFloat
|
||||||
|
var controlsHidden: Bool
|
||||||
|
var isMinimized: Bool
|
||||||
|
var isAnimatedOut: Bool
|
||||||
|
|
||||||
|
init(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool) {
|
||||||
|
self.size = size
|
||||||
|
self.insets = insets
|
||||||
|
self.cornerRadius = cornerRadius
|
||||||
|
self.controlsHidden = controlsHidden
|
||||||
|
self.isMinimized = isMinimized
|
||||||
|
self.isAnimatedOut = isAnimatedOut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct VideoMetrics: Equatable {
|
||||||
|
var resolution: CGSize
|
||||||
|
var rotationAngle: Float
|
||||||
|
var sourceId: Int
|
||||||
|
|
||||||
|
init(resolution: CGSize, rotationAngle: Float, sourceId: Int) {
|
||||||
|
self.resolution = resolution
|
||||||
|
self.rotationAngle = rotationAngle
|
||||||
|
self.sourceId = sourceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class FlipAnimationInfo {
|
||||||
|
let isForward: Bool
|
||||||
|
|
||||||
|
init(isForward: Bool) {
|
||||||
|
self.isForward = isForward
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class DisappearingVideo {
|
||||||
|
let flipAnimationInfo: FlipAnimationInfo?
|
||||||
|
let videoLayer: PrivateCallVideoLayer
|
||||||
|
let videoMetrics: VideoMetrics
|
||||||
|
var isAlphaAnimationInitiated: Bool = false
|
||||||
|
|
||||||
|
init(flipAnimationInfo: FlipAnimationInfo?, videoLayer: PrivateCallVideoLayer, videoMetrics: VideoMetrics) {
|
||||||
|
self.flipAnimationInfo = flipAnimationInfo
|
||||||
|
self.videoLayer = videoLayer
|
||||||
|
self.videoMetrics = videoMetrics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum MinimizedPosition: CaseIterable {
|
||||||
|
case topLeft
|
||||||
|
case topRight
|
||||||
|
case bottomLeft
|
||||||
|
case bottomRight
|
||||||
|
}
|
||||||
|
|
||||||
|
let key: Key
|
||||||
|
|
||||||
|
private let videoContainerLayer: VideoContainerLayer
|
||||||
|
|
||||||
|
private var videoLayer: PrivateCallVideoLayer
|
||||||
|
private var disappearingVideoLayer: DisappearingVideo?
|
||||||
|
|
||||||
|
let blurredContainerLayer: SimpleLayer
|
||||||
|
|
||||||
|
private let shadowContainer: SimpleLayer
|
||||||
|
private let topShadowLayer: SimpleLayer
|
||||||
|
private let bottomShadowLayer: SimpleLayer
|
||||||
|
|
||||||
|
private var params: Params?
|
||||||
|
private var videoMetrics: VideoMetrics?
|
||||||
|
private var appliedVideoMetrics: VideoMetrics?
|
||||||
|
|
||||||
|
private var highlightedState: Bool = false
|
||||||
|
|
||||||
|
private(set) var isFillingBounds: Bool = false
|
||||||
|
|
||||||
|
private var minimizedPosition: MinimizedPosition = .bottomRight
|
||||||
|
private var initialDragPosition: CGPoint?
|
||||||
|
private var dragPosition: CGPoint?
|
||||||
|
private var dragVelocity: CGPoint = CGPoint()
|
||||||
|
private var dragPositionAnimatorLink: SharedDisplayLinkDriver.Link?
|
||||||
|
|
||||||
|
private var videoOnUpdatedListener: Disposable?
|
||||||
|
var video: VideoSource? {
|
||||||
|
didSet {
|
||||||
|
if self.video !== oldValue {
|
||||||
|
self.videoOnUpdatedListener?.dispose()
|
||||||
|
|
||||||
|
self.videoOnUpdatedListener = self.video?.addOnUpdated { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var videoMetrics: VideoMetrics?
|
||||||
|
if let currentOutput = self.video?.currentOutput {
|
||||||
|
if let previousVideo = self.videoLayer.video, previousVideo.sourceId != currentOutput.sourceId {
|
||||||
|
self.initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo(isForward: previousVideo.sourceId < currentOutput.sourceId))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.videoLayer.video = currentOutput
|
||||||
|
videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
|
||||||
|
} else {
|
||||||
|
self.videoLayer.video = nil
|
||||||
|
}
|
||||||
|
self.videoLayer.setNeedsUpdate()
|
||||||
|
|
||||||
|
if self.videoMetrics != videoMetrics {
|
||||||
|
self.videoMetrics = videoMetrics
|
||||||
|
self.update(transition: .easeInOut(duration: 0.2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldValue != nil {
|
||||||
|
self.initiateVideoSourceSwitch(flipAnimationInfo: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var videoMetrics: VideoMetrics?
|
||||||
|
if let currentOutput = self.video?.currentOutput {
|
||||||
|
self.videoLayer.video = currentOutput
|
||||||
|
videoMetrics = VideoMetrics(resolution: currentOutput.resolution, rotationAngle: currentOutput.rotationAngle, sourceId: currentOutput.sourceId)
|
||||||
|
} else {
|
||||||
|
self.videoLayer.video = nil
|
||||||
|
}
|
||||||
|
self.videoLayer.setNeedsUpdate()
|
||||||
|
|
||||||
|
if self.videoMetrics != videoMetrics || oldValue != nil {
|
||||||
|
self.videoMetrics = videoMetrics
|
||||||
|
self.update(transition: .easeInOut(duration: 0.2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pressAction: (() -> Void)?
|
||||||
|
|
||||||
|
init(key: Key) {
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
self.videoContainerLayer = VideoContainerLayer()
|
||||||
|
self.videoContainerLayer.backgroundColor = nil
|
||||||
|
self.videoContainerLayer.isOpaque = false
|
||||||
|
self.videoContainerLayer.contentsLayer.backgroundColor = nil
|
||||||
|
self.videoContainerLayer.contentsLayer.isOpaque = false
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
self.videoContainerLayer.contentsLayer.cornerCurve = .circular
|
||||||
|
}
|
||||||
|
|
||||||
|
self.videoLayer = PrivateCallVideoLayer()
|
||||||
|
self.videoLayer.masksToBounds = true
|
||||||
|
self.videoLayer.isDoubleSided = false
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
self.videoLayer.cornerCurve = .circular
|
||||||
|
}
|
||||||
|
|
||||||
|
self.blurredContainerLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.shadowContainer = SimpleLayer()
|
||||||
|
self.topShadowLayer = SimpleLayer()
|
||||||
|
self.topShadowLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
|
||||||
|
self.bottomShadowLayer = SimpleLayer()
|
||||||
|
|
||||||
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
|
self.videoContainerLayer.contentsLayer.addSublayer(self.videoLayer)
|
||||||
|
self.layer.addSublayer(self.videoContainerLayer)
|
||||||
|
self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer)
|
||||||
|
|
||||||
|
self.topShadowLayer.contents = shadowImage?.cgImage
|
||||||
|
self.bottomShadowLayer.contents = shadowImage?.cgImage
|
||||||
|
self.shadowContainer.addSublayer(self.topShadowLayer)
|
||||||
|
self.shadowContainer.addSublayer(self.bottomShadowLayer)
|
||||||
|
self.layer.addSublayer(self.shadowContainer)
|
||||||
|
|
||||||
|
self.highligthedChanged = { [weak self] highlighted in
|
||||||
|
guard let self, let params = self.params, !self.videoContainerLayer.bounds.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var highlightedState = false
|
||||||
|
if highlighted {
|
||||||
|
if params.isMinimized {
|
||||||
|
highlightedState = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
highlightedState = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.highlightedState == highlightedState {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.highlightedState = highlightedState
|
||||||
|
|
||||||
|
let measurementSide = min(self.videoContainerLayer.bounds.width, self.videoContainerLayer.bounds.height)
|
||||||
|
let topScale: CGFloat = (measurementSide - 8.0) / measurementSide
|
||||||
|
let maxScale: CGFloat = (measurementSide + 2.0) / measurementSide
|
||||||
|
|
||||||
|
if highlightedState {
|
||||||
|
self.videoContainerLayer.removeAnimation(forKey: "sublayerTransform")
|
||||||
|
let transition = Transition(animation: .curve(duration: 0.15, curve: .easeInOut))
|
||||||
|
transition.setSublayerTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeScale(topScale, topScale, 1.0))
|
||||||
|
} else {
|
||||||
|
let t = self.videoContainerLayer.presentation()?.sublayerTransform ?? self.videoContainerLayer.sublayerTransform
|
||||||
|
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||||
|
|
||||||
|
let transition = Transition(animation: .none)
|
||||||
|
transition.setSublayerTransform(layer: self.videoContainerLayer, transform: CATransform3DIdentity)
|
||||||
|
|
||||||
|
self.videoContainerLayer.animateSublayerScale(from: currentScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] completed in
|
||||||
|
guard let self, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.videoContainerLayer.animateSublayerScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
guard let params = self.params else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if params.isMinimized {
|
||||||
|
let videoContainerPoint = self.layer.convert(point, to: self.videoContainerLayer)
|
||||||
|
if self.videoContainerLayer.bounds.contains(videoContainerPoint) {
|
||||||
|
return self
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
self.pressAction?()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began, .changed:
|
||||||
|
self.dragVelocity = CGPoint()
|
||||||
|
if let dragPositionAnimatorLink = self.dragPositionAnimatorLink {
|
||||||
|
self.dragPositionAnimatorLink = nil
|
||||||
|
dragPositionAnimatorLink.invalidate()
|
||||||
|
}
|
||||||
|
let translation = recognizer.translation(in: self)
|
||||||
|
|
||||||
|
let initialDragPosition: CGPoint
|
||||||
|
if let current = self.initialDragPosition {
|
||||||
|
initialDragPosition = current
|
||||||
|
} else {
|
||||||
|
initialDragPosition = self.videoContainerLayer.position
|
||||||
|
self.initialDragPosition = initialDragPosition
|
||||||
|
}
|
||||||
|
self.dragPosition = initialDragPosition.offsetBy(dx: translation.x, dy: translation.y)
|
||||||
|
self.update(transition: .immediate)
|
||||||
|
case .ended, .cancelled:
|
||||||
|
self.initialDragPosition = nil
|
||||||
|
self.dragVelocity = recognizer.velocity(in: self)
|
||||||
|
|
||||||
|
if let params = self.params, let dragPosition = self.dragPosition {
|
||||||
|
let endPosition = CGPoint(
|
||||||
|
x: dragPosition.x - self.dragVelocity.x / (1000.0 * log(0.99)),
|
||||||
|
y: dragPosition.y - self.dragVelocity.y / (1000.0 * log(0.99))
|
||||||
|
)
|
||||||
|
|
||||||
|
var minCornerDistance: (corner: MinimizedPosition, distance: CGFloat)?
|
||||||
|
for corner in MinimizedPosition.allCases {
|
||||||
|
let cornerPosition: CGPoint
|
||||||
|
switch corner {
|
||||||
|
case .topLeft:
|
||||||
|
cornerPosition = CGPoint(x: params.insets.left, y: params.insets.top)
|
||||||
|
case .topRight:
|
||||||
|
cornerPosition = CGPoint(x: params.size.width - params.insets.right, y: params.insets.top)
|
||||||
|
case .bottomLeft:
|
||||||
|
cornerPosition = CGPoint(x: params.insets.left, y: params.size.height - params.insets.bottom)
|
||||||
|
case .bottomRight:
|
||||||
|
cornerPosition = CGPoint(x: params.size.width - params.insets.right, y: params.size.height - params.insets.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
let distance = CGPoint(x: endPosition.x - cornerPosition.x, y: endPosition.y - cornerPosition.y)
|
||||||
|
let scalarDistance = sqrt(distance.x * distance.x + distance.y * distance.y)
|
||||||
|
if let (_, minDistance) = minCornerDistance {
|
||||||
|
if scalarDistance < minDistance {
|
||||||
|
minCornerDistance = (corner, scalarDistance)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
minCornerDistance = (corner, scalarDistance)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let minCornerDistance {
|
||||||
|
self.minimizedPosition = minCornerDistance.corner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.dragPositionAnimatorLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.updateDragPositionAnimation(deltaTime: deltaTime)
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateVelocityUsingSpring(currentVelocity: CGPoint, currentPosition: CGPoint, attractor: CGPoint, springConstant: CGFloat, damping: CGFloat, deltaTime: CGFloat) -> CGPoint {
|
||||||
|
let displacement = CGPoint(x: attractor.x - currentPosition.x, y: attractor.y - currentPosition.y)
|
||||||
|
let springForce = CGPoint(x: -springConstant * displacement.x, y: -springConstant * displacement.y)
|
||||||
|
var newVelocity = CGPoint(x: currentVelocity.x + springForce.x * deltaTime, y: currentVelocity.y + springForce.y * deltaTime)
|
||||||
|
newVelocity = CGPoint(x: newVelocity.x * exp(-damping * deltaTime), y: newVelocity.y * exp(-damping * deltaTime))
|
||||||
|
return newVelocity
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateDragPositionAnimation(deltaTime: Double) {
|
||||||
|
guard let params = self.params, let videoMetrics = self.videoMetrics else {
|
||||||
|
self.dragPosition = nil
|
||||||
|
self.dragPositionAnimatorLink = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !params.isMinimized {
|
||||||
|
self.dragPosition = nil
|
||||||
|
self.dragPositionAnimatorLink = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard var dragPosition = self.dragPosition else {
|
||||||
|
self.dragPosition = nil
|
||||||
|
self.dragPositionAnimatorLink = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: false)
|
||||||
|
let targetPosition = videoLayout.rotatedVideoFrame.center
|
||||||
|
|
||||||
|
self.dragVelocity = self.updateVelocityUsingSpring(
|
||||||
|
currentVelocity: self.dragVelocity,
|
||||||
|
currentPosition: dragPosition,
|
||||||
|
attractor: targetPosition,
|
||||||
|
springConstant: -130.0,
|
||||||
|
damping: 17.0,
|
||||||
|
deltaTime: CGFloat(deltaTime)
|
||||||
|
)
|
||||||
|
|
||||||
|
if sqrt(self.dragVelocity.x * self.dragVelocity.x + self.dragVelocity.y * self.dragVelocity.y) <= 0.1 {
|
||||||
|
self.dragVelocity = CGPoint()
|
||||||
|
self.dragPosition = nil
|
||||||
|
self.dragPositionAnimatorLink = nil
|
||||||
|
} else {
|
||||||
|
dragPosition.x += self.dragVelocity.x * CGFloat(deltaTime)
|
||||||
|
dragPosition.y += self.dragVelocity.y * CGFloat(deltaTime)
|
||||||
|
|
||||||
|
self.dragPosition = dragPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update(transition: .immediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initiateVideoSourceSwitch(flipAnimationInfo: FlipAnimationInfo?) {
|
||||||
|
guard let videoMetrics = self.videoMetrics else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let disappearingVideoLayer = self.disappearingVideoLayer {
|
||||||
|
disappearingVideoLayer.videoLayer.removeFromSuperlayer()
|
||||||
|
disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer()
|
||||||
|
}
|
||||||
|
let previousVideoLayer = self.videoLayer
|
||||||
|
self.disappearingVideoLayer = DisappearingVideo(flipAnimationInfo: flipAnimationInfo, videoLayer: self.videoLayer, videoMetrics: videoMetrics)
|
||||||
|
|
||||||
|
self.videoLayer = PrivateCallVideoLayer()
|
||||||
|
self.videoLayer.opacity = previousVideoLayer.opacity
|
||||||
|
self.videoLayer.masksToBounds = true
|
||||||
|
self.videoLayer.isDoubleSided = false
|
||||||
|
if #available(iOS 13.0, *) {
|
||||||
|
self.videoLayer.cornerCurve = .circular
|
||||||
|
}
|
||||||
|
self.videoLayer.cornerRadius = previousVideoLayer.cornerRadius
|
||||||
|
self.videoLayer.blurredLayer.opacity = previousVideoLayer.blurredLayer.opacity
|
||||||
|
|
||||||
|
self.videoContainerLayer.contentsLayer.addSublayer(self.videoLayer)
|
||||||
|
self.blurredContainerLayer.addSublayer(self.videoLayer.blurredLayer)
|
||||||
|
|
||||||
|
self.dragPosition = nil
|
||||||
|
self.dragPositionAnimatorLink = nil
|
||||||
|
}
|
||||||
|
|
||||||
private func update(transition: Transition) {
|
private func update(transition: Transition) {
|
||||||
guard let params = self.params else {
|
guard let params = self.params else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.update(params: params, transition: transition)
|
self.update(previousParams: params, params: params, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, isMinimized: Bool, isAnimatingOut: Bool, transition: Transition) {
|
func update(size: CGSize, insets: UIEdgeInsets, cornerRadius: CGFloat, controlsHidden: Bool, isMinimized: Bool, isAnimatedOut: Bool, transition: Transition) {
|
||||||
let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, isMinimized: isMinimized, isAnimatingOut: isAnimatingOut)
|
let params = Params(size: size, insets: insets, cornerRadius: cornerRadius, controlsHidden: controlsHidden, isMinimized: isMinimized, isAnimatedOut: isAnimatedOut)
|
||||||
if self.params == params {
|
if self.params == params {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.layer.masksToBounds = true
|
let previousParams = self.params
|
||||||
if self.layer.animation(forKey: "cornerRadius") == nil {
|
|
||||||
self.layer.cornerRadius = self.params?.cornerRadius ?? 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
self.params = params
|
self.params = params
|
||||||
|
|
||||||
transition.setCornerRadius(layer: self.layer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in
|
if let previousParams, previousParams.controlsHidden != params.controlsHidden {
|
||||||
guard let self, let params = self.params, completed else {
|
self.dragPosition = nil
|
||||||
return
|
self.dragPositionAnimatorLink = nil
|
||||||
}
|
|
||||||
if !params.isAnimatingOut {
|
|
||||||
self.layer.masksToBounds = false
|
|
||||||
self.layer.cornerRadius = 0.0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
self.update(params: params, transition: transition)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func update(params: Params, transition: Transition) {
|
self.update(previousParams: previousParams, params: params, transition: transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MinimizedLayout {
|
||||||
|
var videoIsRotated: Bool
|
||||||
|
var rotatedVideoSize: CGSize
|
||||||
|
var rotatedVideoResolution: CGSize
|
||||||
|
var rotatedVideoFrame: CGRect
|
||||||
|
var videoTransform: CATransform3D
|
||||||
|
var effectiveVideoFrame: CGRect
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calculateMinimizedLayout(params: Params, videoMetrics: VideoMetrics, applyDragPosition: Bool) -> MinimizedLayout {
|
||||||
|
var rotatedResolution = videoMetrics.resolution
|
||||||
|
var videoIsRotated = false
|
||||||
|
if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 {
|
||||||
|
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
|
||||||
|
videoIsRotated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let minimizedBoundingSize: CGFloat = params.controlsHidden ? 140.0 : 240.0
|
||||||
|
let videoSize = rotatedResolution.aspectFitted(CGSize(width: minimizedBoundingSize, height: minimizedBoundingSize))
|
||||||
|
|
||||||
|
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
|
||||||
|
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
|
||||||
|
|
||||||
|
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
||||||
|
|
||||||
|
let rotatedVideoFrame: CGRect
|
||||||
|
if applyDragPosition, let dragPosition = self.dragPosition {
|
||||||
|
rotatedVideoFrame = videoSize.centered(around: dragPosition)
|
||||||
|
} else {
|
||||||
|
switch self.minimizedPosition {
|
||||||
|
case .topLeft:
|
||||||
|
rotatedVideoFrame = CGRect(origin: CGPoint(x: params.insets.left, y: params.insets.top), size: videoSize)
|
||||||
|
case .topRight:
|
||||||
|
rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.insets.top), size: videoSize)
|
||||||
|
case .bottomLeft:
|
||||||
|
rotatedVideoFrame = CGRect(origin: CGPoint(x: params.insets.left, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize)
|
||||||
|
case .bottomRight:
|
||||||
|
rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let effectiveVideoFrame = videoSize.centered(around: rotatedVideoFrame.center)
|
||||||
|
|
||||||
|
var videoTransform = CATransform3DIdentity
|
||||||
|
videoTransform.m34 = 1.0 / 600.0
|
||||||
|
videoTransform = CATransform3DRotate(videoTransform, CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0)
|
||||||
|
if params.isAnimatedOut {
|
||||||
|
videoTransform = CATransform3DScale(videoTransform, 0.6, 0.6, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MinimizedLayout(
|
||||||
|
videoIsRotated: videoIsRotated,
|
||||||
|
rotatedVideoSize: rotatedVideoSize,
|
||||||
|
rotatedVideoResolution: rotatedVideoResolution,
|
||||||
|
rotatedVideoFrame: rotatedVideoFrame,
|
||||||
|
videoTransform: videoTransform,
|
||||||
|
effectiveVideoFrame: effectiveVideoFrame
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(previousParams: Params?, params: Params, transition: Transition) {
|
||||||
guard let videoMetrics = self.videoMetrics else {
|
guard let videoMetrics = self.videoMetrics else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -151,43 +529,108 @@ final class VideoContainerView: UIView {
|
|||||||
self.appliedVideoMetrics = videoMetrics
|
self.appliedVideoMetrics = videoMetrics
|
||||||
|
|
||||||
if params.isMinimized {
|
if params.isMinimized {
|
||||||
var rotatedResolution = videoMetrics.resolution
|
self.isFillingBounds = false
|
||||||
var videoIsRotated = false
|
|
||||||
if videoMetrics.rotationAngle == Float.pi * 0.5 || videoMetrics.rotationAngle == Float.pi * 3.0 / 2.0 {
|
let videoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: videoMetrics, applyDragPosition: true)
|
||||||
rotatedResolution = CGSize(width: rotatedResolution.height, height: rotatedResolution.width)
|
|
||||||
videoIsRotated = true
|
transition.setPosition(layer: self.videoContainerLayer, position: videoLayout.rotatedVideoFrame.center)
|
||||||
|
|
||||||
|
self.videoContainerLayer.contentsLayer.masksToBounds = true
|
||||||
|
if self.disappearingVideoLayer != nil {
|
||||||
|
self.videoContainerLayer.contentsLayer.backgroundColor = UIColor.black.cgColor
|
||||||
|
}
|
||||||
|
transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize), completion: { [weak self] completed in
|
||||||
|
guard let self, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.videoContainerLayer.contentsLayer.masksToBounds = false
|
||||||
|
self.videoContainerLayer.contentsLayer.backgroundColor = nil
|
||||||
|
})
|
||||||
|
self.videoContainerLayer.update(size: videoLayout.rotatedVideoSize, transition: transition)
|
||||||
|
|
||||||
|
var videoTransition = transition
|
||||||
|
if self.videoLayer.bounds.isEmpty {
|
||||||
|
videoTransition = .immediate
|
||||||
|
}
|
||||||
|
var animateFlipDisappearingVideo: DisappearingVideo?
|
||||||
|
if let disappearingVideoLayer = self.disappearingVideoLayer {
|
||||||
|
self.disappearingVideoLayer = nil
|
||||||
|
|
||||||
|
let disappearingVideoLayout = self.calculateMinimizedLayout(params: params, videoMetrics: disappearingVideoLayer.videoMetrics, applyDragPosition: true)
|
||||||
|
let initialDisapparingVideoSize = disappearingVideoLayout.rotatedVideoSize
|
||||||
|
|
||||||
|
if !disappearingVideoLayer.isAlphaAnimationInitiated {
|
||||||
|
disappearingVideoLayer.isAlphaAnimationInitiated = true
|
||||||
|
|
||||||
|
if let flipAnimationInfo = disappearingVideoLayer.flipAnimationInfo {
|
||||||
|
var videoTransform = self.videoContainerLayer.transform
|
||||||
|
videoTransform = CATransform3DRotate(videoTransform, (flipAnimationInfo.isForward ? 1.0 : -1.0) * CGFloat.pi * 0.9999, 0.0, 1.0, 0.0)
|
||||||
|
self.videoContainerLayer.transform = videoTransform
|
||||||
|
|
||||||
|
disappearingVideoLayer.videoLayer.zPosition = 1.0
|
||||||
|
transition.setZPosition(layer: disappearingVideoLayer.videoLayer, zPosition: -1.0)
|
||||||
|
|
||||||
|
disappearingVideoLayer.videoLayer.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||||||
|
|
||||||
|
animateFlipDisappearingVideo = disappearingVideoLayer
|
||||||
|
disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer()
|
||||||
|
} else {
|
||||||
|
let alphaTransition: Transition = .easeInOut(duration: 0.2)
|
||||||
|
let disappearingVideoLayerValue = disappearingVideoLayer.videoLayer
|
||||||
|
alphaTransition.setAlpha(layer: disappearingVideoLayerValue, alpha: 0.0, completion: { [weak self, weak disappearingVideoLayerValue] _ in
|
||||||
|
guard let self, let disappearingVideoLayerValue else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
disappearingVideoLayerValue.removeFromSuperlayer()
|
||||||
|
if self.disappearingVideoLayer?.videoLayer === disappearingVideoLayerValue {
|
||||||
|
self.disappearingVideoLayer = nil
|
||||||
|
self.update(transition: .immediate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
disappearingVideoLayer.videoLayer.blurredLayer.removeFromSuperlayer()
|
||||||
|
|
||||||
|
self.videoLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
}
|
}
|
||||||
|
|
||||||
let videoSize = rotatedResolution.aspectFitted(CGSize(width: 160.0, height: 160.0))
|
self.videoLayer.position = disappearingVideoLayer.videoLayer.position
|
||||||
|
self.videoLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(initialDisapparingVideoSize))
|
||||||
|
self.videoLayer.blurredLayer.position = disappearingVideoLayer.videoLayer.blurredLayer.position
|
||||||
|
self.videoLayer.blurredLayer.bounds = CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize.aspectFilled(initialDisapparingVideoSize))
|
||||||
|
}
|
||||||
|
|
||||||
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
|
let disappearingVideoSize = initialDisapparingVideoSize.aspectFilled(videoLayout.rotatedVideoSize)
|
||||||
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
|
transition.setPosition(layer: disappearingVideoLayer.videoLayer, position: CGPoint(x: videoLayout.rotatedVideoSize.width * 0.5, y: videoLayout.rotatedVideoSize.height * 0.5))
|
||||||
|
transition.setBounds(layer: disappearingVideoLayer.videoLayer, bounds: CGRect(origin: CGPoint(), size: disappearingVideoSize))
|
||||||
|
transition.setPosition(layer: disappearingVideoLayer.videoLayer.blurredLayer, position: videoLayout.rotatedVideoFrame.center)
|
||||||
|
transition.setBounds(layer: disappearingVideoLayer.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: disappearingVideoSize))
|
||||||
|
}
|
||||||
|
|
||||||
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
let animateFlipDisappearingVideoLayer = animateFlipDisappearingVideo?.videoLayer
|
||||||
let rotatedVideoFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - videoSize.width, y: params.size.height - params.insets.bottom - videoSize.height), size: videoSize)
|
transition.setTransform(layer: self.videoContainerLayer, transform: videoLayout.videoTransform, completion: { [weak animateFlipDisappearingVideoLayer] _ in
|
||||||
let effectiveVideoFrame = videoSize.centered(around: rotatedVideoFrame.center)
|
animateFlipDisappearingVideoLayer?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
|
||||||
transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center)
|
transition.setPosition(layer: self.videoLayer, position: CGPoint(x: videoLayout.rotatedVideoSize.width * 0.5, y: videoLayout.rotatedVideoSize.height * 0.5))
|
||||||
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
|
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize))
|
||||||
transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
|
|
||||||
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoSize))
|
|
||||||
|
|
||||||
transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
transition.setPosition(layer: self.videoLayer.blurredLayer, position: videoLayout.rotatedVideoFrame.center)
|
||||||
transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
transition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 0.0)
|
||||||
|
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: videoLayout.rotatedVideoSize))
|
||||||
|
videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: videoLayout.videoTransform)
|
||||||
|
|
||||||
transition.setCornerRadius(layer: self.videoLayer, cornerRadius: 10.0)
|
if let previousParams, !previousParams.isMinimized {
|
||||||
|
self.videoContainerLayer.contentsLayer.cornerRadius = previousParams.cornerRadius
|
||||||
|
}
|
||||||
|
transition.setCornerRadius(layer: self.videoContainerLayer.contentsLayer, cornerRadius: 18.0, completion: { [weak self] completed in
|
||||||
|
guard let self, completed, let params = self.params else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if params.isMinimized {
|
||||||
|
self.videoLayer.cornerRadius = 18.0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
|
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(videoLayout.rotatedVideoResolution.width), height: Int(videoLayout.rotatedVideoResolution.height)), edgeInset: 2)
|
||||||
|
|
||||||
let topShadowHeight: CGFloat = floor(effectiveVideoFrame.height * 0.2)
|
|
||||||
let topShadowFrame = CGRect(origin: effectiveVideoFrame.origin, size: CGSize(width: effectiveVideoFrame.width, height: topShadowHeight))
|
|
||||||
transition.setPosition(view: self.topShadowView, position: topShadowFrame.center)
|
|
||||||
transition.setBounds(view: self.topShadowView, bounds: CGRect(origin: CGPoint(x: effectiveVideoFrame.minX, y: effectiveVideoFrame.maxY - topShadowHeight), size: topShadowFrame.size))
|
|
||||||
transition.setAlpha(view: self.topShadowView, alpha: 0.0)
|
|
||||||
|
|
||||||
let bottomShadowHeight: CGFloat = 200.0
|
|
||||||
transition.setFrame(view: self.bottomShadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight)))
|
|
||||||
transition.setAlpha(view: self.bottomShadowView, alpha: 0.0)
|
|
||||||
} else {
|
} else {
|
||||||
var rotatedResolution = videoMetrics.resolution
|
var rotatedResolution = videoMetrics.resolution
|
||||||
var videoIsRotated = false
|
var videoIsRotated = false
|
||||||
@ -196,41 +639,109 @@ final class VideoContainerView: UIView {
|
|||||||
videoIsRotated = true
|
videoIsRotated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var videoSize = rotatedResolution.aspectFitted(params.size)
|
var videoSize: CGSize
|
||||||
|
if params.isAnimatedOut {
|
||||||
|
self.isFillingBounds = true
|
||||||
|
videoSize = rotatedResolution.aspectFilled(params.size)
|
||||||
|
} else {
|
||||||
|
videoSize = rotatedResolution.aspectFitted(params.size)
|
||||||
let boundingAspectRatio = params.size.width / params.size.height
|
let boundingAspectRatio = params.size.width / params.size.height
|
||||||
let videoAspectRatio = videoSize.width / videoSize.height
|
let videoAspectRatio = videoSize.width / videoSize.height
|
||||||
if abs(boundingAspectRatio - videoAspectRatio) < 0.15 {
|
self.isFillingBounds = abs(boundingAspectRatio - videoAspectRatio) < 0.15
|
||||||
|
if self.isFillingBounds {
|
||||||
videoSize = rotatedResolution.aspectFilled(params.size)
|
videoSize = rotatedResolution.aspectFilled(params.size)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
|
let videoResolution = rotatedResolution.aspectFittedOrSmaller(CGSize(width: 1280, height: 1280)).aspectFittedOrSmaller(CGSize(width: videoSize.width * 3.0, height: videoSize.height * 3.0))
|
||||||
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
|
let rotatedVideoResolution = videoIsRotated ? CGSize(width: videoResolution.height, height: videoResolution.width) : videoResolution
|
||||||
|
|
||||||
|
let rotatedBoundingSize = videoIsRotated ? CGSize(width: params.size.height, height: params.size.width) : params.size
|
||||||
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
let rotatedVideoSize = videoIsRotated ? CGSize(width: videoSize.height, height: videoSize.width) : videoSize
|
||||||
let rotatedBoundingSize = params.size
|
let rotatedVideoBoundingSize = params.size
|
||||||
let rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((rotatedBoundingSize.width - rotatedVideoSize.width) * 0.5), y: floor((rotatedBoundingSize.height - rotatedVideoSize.height) * 0.5)), size: rotatedVideoSize)
|
let rotatedVideoFrame = CGRect(origin: CGPoint(x: floor((rotatedVideoBoundingSize.width - rotatedVideoSize.width) * 0.5), y: floor((rotatedVideoBoundingSize.height - rotatedVideoSize.height) * 0.5)), size: rotatedVideoSize)
|
||||||
|
|
||||||
transition.setPosition(layer: self.videoLayer, position: rotatedVideoFrame.center)
|
self.videoContainerLayer.contentsLayer.masksToBounds = true
|
||||||
transition.setBounds(layer: self.videoLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
|
if let previousParams, self.videoContainerLayer.contentsLayer.animation(forKey: "cornerRadius") == nil {
|
||||||
transition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
|
if previousParams.isMinimized {
|
||||||
transition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
|
self.videoContainerLayer.contentsLayer.cornerRadius = self.videoLayer.cornerRadius
|
||||||
|
} else {
|
||||||
transition.setTransform(layer: self.videoLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
self.videoContainerLayer.contentsLayer.cornerRadius = previousParams.cornerRadius
|
||||||
transition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
|
||||||
|
|
||||||
if !params.isAnimatingOut {
|
|
||||||
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
self.videoLayer.cornerRadius = 0.0
|
||||||
|
transition.setCornerRadius(layer: self.videoContainerLayer.contentsLayer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in
|
||||||
|
guard let self, completed, let params = self.params else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !params.isMinimized && !params.isAnimatedOut {
|
||||||
|
self.videoContainerLayer.contentsLayer.cornerRadius = 0.0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
transition.setPosition(layer: self.videoContainerLayer, position: CGPoint(x: params.size.width * 0.5, y: params.size.height * 0.5))
|
||||||
|
transition.setBounds(layer: self.videoContainerLayer, bounds: CGRect(origin: CGPoint(), size: rotatedBoundingSize))
|
||||||
|
self.videoContainerLayer.update(size: rotatedBoundingSize, transition: transition)
|
||||||
|
|
||||||
|
var videoTransition = transition
|
||||||
|
if self.videoLayer.bounds.isEmpty {
|
||||||
|
videoTransition = .immediate
|
||||||
|
if !transition.animation.isImmediate {
|
||||||
|
self.videoLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
self.videoLayer.blurredLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let disappearingVideoLayer = self.disappearingVideoLayer {
|
||||||
|
self.disappearingVideoLayer = nil
|
||||||
|
|
||||||
|
if !disappearingVideoLayer.isAlphaAnimationInitiated {
|
||||||
|
disappearingVideoLayer.isAlphaAnimationInitiated = true
|
||||||
|
|
||||||
|
let alphaTransition: Transition = .easeInOut(duration: 0.2)
|
||||||
|
let disappearingVideoLayerValue = disappearingVideoLayer.videoLayer
|
||||||
|
alphaTransition.setAlpha(layer: disappearingVideoLayerValue, alpha: 0.0, completion: { [weak disappearingVideoLayerValue] _ in
|
||||||
|
disappearingVideoLayerValue?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
let disappearingVideoLayerBlurredLayerValue = disappearingVideoLayer.videoLayer.blurredLayer
|
||||||
|
alphaTransition.setAlpha(layer: disappearingVideoLayerBlurredLayerValue, alpha: 0.0, completion: { [weak disappearingVideoLayerBlurredLayerValue] _ in
|
||||||
|
disappearingVideoLayerBlurredLayerValue?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setTransform(layer: self.videoContainerLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
||||||
|
|
||||||
|
videoTransition.setFrame(layer: self.videoLayer, frame: rotatedVideoSize.centered(around: CGPoint(x: rotatedBoundingSize.width * 0.5, y: rotatedBoundingSize.height * 0.5)))
|
||||||
|
videoTransition.setPosition(layer: self.videoLayer.blurredLayer, position: rotatedVideoFrame.center)
|
||||||
|
videoTransition.setBounds(layer: self.videoLayer.blurredLayer, bounds: CGRect(origin: CGPoint(), size: rotatedVideoFrame.size))
|
||||||
|
videoTransition.setAlpha(layer: self.videoLayer.blurredLayer, alpha: 1.0)
|
||||||
|
videoTransition.setTransform(layer: self.videoLayer.blurredLayer, transform: CATransform3DMakeRotation(CGFloat(videoMetrics.rotationAngle), 0.0, 0.0, 1.0))
|
||||||
|
|
||||||
|
if !params.isAnimatedOut {
|
||||||
|
self.videoLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(rotatedVideoResolution.width), height: Int(rotatedVideoResolution.height)), edgeInset: 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shadowContainer.masksToBounds = true
|
||||||
|
transition.setCornerRadius(layer: self.shadowContainer, cornerRadius: params.cornerRadius, completion: { [weak self] completed in
|
||||||
|
guard let self, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.shadowContainer.masksToBounds = false
|
||||||
|
})
|
||||||
|
transition.setFrame(layer: self.shadowContainer, frame: CGRect(origin: CGPoint(), size: params.size))
|
||||||
|
|
||||||
|
let shadowAlpha: CGFloat = (params.controlsHidden || params.isMinimized || params.isAnimatedOut) ? 0.0 : 1.0
|
||||||
|
|
||||||
let topShadowHeight: CGFloat = 200.0
|
let topShadowHeight: CGFloat = 200.0
|
||||||
let topShadowFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.size.width, height: topShadowHeight))
|
let topShadowFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: params.size.width, height: topShadowHeight))
|
||||||
transition.setPosition(view: self.topShadowView, position: topShadowFrame.center)
|
transition.setPosition(layer: self.topShadowLayer, position: topShadowFrame.center)
|
||||||
transition.setBounds(view: self.topShadowView, bounds: CGRect(origin: CGPoint(), size: topShadowFrame.size))
|
transition.setBounds(layer: self.topShadowLayer, bounds: CGRect(origin: CGPoint(), size: topShadowFrame.size))
|
||||||
transition.setAlpha(view: self.topShadowView, alpha: 1.0)
|
transition.setAlpha(layer: self.topShadowLayer, alpha: shadowAlpha)
|
||||||
|
|
||||||
let bottomShadowHeight: CGFloat = 200.0
|
let bottomShadowHeight: CGFloat = 200.0
|
||||||
transition.setFrame(view: self.bottomShadowView, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight)))
|
transition.setFrame(layer: self.bottomShadowLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: params.size.height - bottomShadowHeight), size: CGSize(width: params.size.width, height: bottomShadowHeight)))
|
||||||
transition.setAlpha(view: self.bottomShadowView, alpha: 1.0)
|
transition.setAlpha(layer: self.bottomShadowLayer, alpha: shadowAlpha)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import ComponentFlow
|
||||||
|
|
||||||
|
final class VideoShadowsView: UIView {
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(size: CGSize, transition: Transition) {
|
||||||
|
}
|
||||||
|
}
|
@ -2,14 +2,17 @@ import AVFoundation
|
|||||||
import Metal
|
import Metal
|
||||||
import CoreVideo
|
import CoreVideo
|
||||||
import Display
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
public final class VideoSourceOutput {
|
public final class VideoSourceOutput {
|
||||||
|
public let resolution: CGSize
|
||||||
public let y: MTLTexture
|
public let y: MTLTexture
|
||||||
public let uv: MTLTexture
|
public let uv: MTLTexture
|
||||||
public let rotationAngle: Float
|
public let rotationAngle: Float
|
||||||
public let sourceId: Int
|
public let sourceId: Int
|
||||||
|
|
||||||
public init(y: MTLTexture, uv: MTLTexture, rotationAngle: Float, sourceId: Int) {
|
public init(resolution: CGSize, y: MTLTexture, uv: MTLTexture, rotationAngle: Float, sourceId: Int) {
|
||||||
|
self.resolution = resolution
|
||||||
self.y = y
|
self.y = y
|
||||||
self.uv = uv
|
self.uv = uv
|
||||||
self.rotationAngle = rotationAngle
|
self.rotationAngle = rotationAngle
|
||||||
@ -20,8 +23,9 @@ public final class VideoSourceOutput {
|
|||||||
public protocol VideoSource: AnyObject {
|
public protocol VideoSource: AnyObject {
|
||||||
typealias Output = VideoSourceOutput
|
typealias Output = VideoSourceOutput
|
||||||
|
|
||||||
var updated: (() -> Void)? { get set }
|
|
||||||
var currentOutput: Output? { get }
|
var currentOutput: Output? { get }
|
||||||
|
|
||||||
|
func addOnUpdated(_ f: @escaping () -> Void) -> Disposable
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class FileVideoSource: VideoSource {
|
public final class FileVideoSource: VideoSource {
|
||||||
@ -35,13 +39,17 @@ public final class FileVideoSource: VideoSource {
|
|||||||
private var targetItem: AVPlayerItem?
|
private var targetItem: AVPlayerItem?
|
||||||
|
|
||||||
public private(set) var currentOutput: Output?
|
public private(set) var currentOutput: Output?
|
||||||
public var updated: (() -> Void)?
|
private var onUpdatedListeners = Bag<() -> Void>()
|
||||||
|
|
||||||
private var displayLink: SharedDisplayLinkDriver.Link?
|
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||||
|
|
||||||
public var sourceId: Int = 0
|
public var sourceId: Int = 0
|
||||||
|
public var fixedRotationAngle: Float?
|
||||||
|
public var sizeMultiplicator: CGPoint = CGPoint(x: 1.0, y: 1.0)
|
||||||
|
|
||||||
|
public init?(device: MTLDevice, url: URL, fixedRotationAngle: Float? = nil) {
|
||||||
|
self.fixedRotationAngle = fixedRotationAngle
|
||||||
|
|
||||||
public init?(device: MTLDevice, url: URL) {
|
|
||||||
self.device = device
|
self.device = device
|
||||||
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
|
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
|
||||||
|
|
||||||
@ -62,11 +70,26 @@ public final class FileVideoSource: VideoSource {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if self.updateOutput() {
|
if self.updateOutput() {
|
||||||
self.updated?()
|
for onUpdated in self.onUpdatedListeners.copyItems() {
|
||||||
|
onUpdated()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func addOnUpdated(_ f: @escaping () -> Void) -> Disposable {
|
||||||
|
let index = self.onUpdatedListeners.add(f)
|
||||||
|
|
||||||
|
return ActionDisposable { [weak self] in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.onUpdatedListeners.remove(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func updateOutput() -> Bool {
|
private func updateOutput() -> Bool {
|
||||||
if self.targetItem !== self.queuePlayer.currentItem {
|
if self.targetItem !== self.queuePlayer.currentItem {
|
||||||
self.targetItem?.remove(self.videoOutput)
|
self.targetItem?.remove(self.videoOutput)
|
||||||
@ -117,9 +140,15 @@ public final class FileVideoSource: VideoSource {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
rotationAngle = Float.pi * 0.5
|
if let fixedRotationAngle = self.fixedRotationAngle {
|
||||||
|
rotationAngle = fixedRotationAngle
|
||||||
|
}
|
||||||
|
|
||||||
self.currentOutput = Output(y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, sourceId: self.sourceId)
|
var resolution = CGSize(width: CGFloat(yTexture.width), height: CGFloat(yTexture.height))
|
||||||
|
resolution.width = floor(resolution.width * self.sizeMultiplicator.x)
|
||||||
|
resolution.height = floor(resolution.height * self.sizeMultiplicator.y)
|
||||||
|
|
||||||
|
self.currentOutput = Output(resolution: resolution, y: yTexture, uv: uvTexture, rotationAngle: rotationAngle, sourceId: self.sourceId)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,28 @@ final class MirroringLayer: SimpleLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var anchorPoint: CGPoint {
|
||||||
|
get {
|
||||||
|
return super.anchorPoint
|
||||||
|
} set(value) {
|
||||||
|
if let targetLayer = self.targetLayer {
|
||||||
|
targetLayer.anchorPoint = value
|
||||||
|
}
|
||||||
|
super.anchorPoint = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override var anchorPointZ: CGFloat {
|
||||||
|
get {
|
||||||
|
return super.anchorPointZ
|
||||||
|
} set(value) {
|
||||||
|
if let targetLayer = self.targetLayer {
|
||||||
|
targetLayer.anchorPointZ = value
|
||||||
|
}
|
||||||
|
super.anchorPointZ = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override var opacity: Float {
|
override var opacity: Float {
|
||||||
get {
|
get {
|
||||||
return super.opacity
|
return super.opacity
|
||||||
|
@ -5,6 +5,53 @@ import MetalEngine
|
|||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
|
||||||
|
/*private final class EdgeTestLayer: MetalEngineSubjectLayer, MetalEngineSubject {
|
||||||
|
final class RenderState: RenderToLayerState {
|
||||||
|
let pipelineState: MTLRenderPipelineState
|
||||||
|
|
||||||
|
required init?(device: MTLDevice) {
|
||||||
|
guard let library = metalLibrary(device: device) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let vertexFunction = library.makeFunction(name: "edgeTestVertex"), let fragmentFunction = library.makeFunction(name: "edgeTestFragment") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let pipelineDescriptor = MTLRenderPipelineDescriptor()
|
||||||
|
pipelineDescriptor.vertexFunction = vertexFunction
|
||||||
|
pipelineDescriptor.fragmentFunction = fragmentFunction
|
||||||
|
pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
|
||||||
|
pipelineDescriptor.colorAttachments[0].isBlendingEnabled = true
|
||||||
|
pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
|
||||||
|
pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
|
||||||
|
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
|
||||||
|
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
|
||||||
|
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
|
||||||
|
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .one
|
||||||
|
|
||||||
|
guard let pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.pipelineState = pipelineState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var internalData: MetalEngineSubjectInternalData?
|
||||||
|
|
||||||
|
func update(context: MetalEngineSubjectContext) {
|
||||||
|
context.renderToLayer(spec: RenderLayerSpec(size: RenderSize(width: 300, height: 300), edgeInset: 100), state: RenderState.self, layer: self, commands: { encoder, placement in
|
||||||
|
let effectiveRect = placement.effectiveRect
|
||||||
|
|
||||||
|
var rect = SIMD4<Float>(Float(effectiveRect.minX), Float(effectiveRect.minY), Float(effectiveRect.width * 0.5), Float(effectiveRect.height))
|
||||||
|
encoder.setVertexBytes(&rect, length: 4 * 4, index: 0)
|
||||||
|
|
||||||
|
var color = SIMD4<Float>(1.0, 0.0, 0.0, 1.0)
|
||||||
|
encoder.setFragmentBytes(&color, length: 4 * 4, index: 0)
|
||||||
|
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
public final class PrivateCallScreen: OverlayMaskContainerView {
|
public final class PrivateCallScreen: OverlayMaskContainerView {
|
||||||
public struct State: Equatable {
|
public struct State: Equatable {
|
||||||
public struct SignalInfo: Equatable {
|
public struct SignalInfo: Equatable {
|
||||||
@ -50,6 +97,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
|
|
||||||
public var lifecycleState: LifecycleState
|
public var lifecycleState: LifecycleState
|
||||||
public var name: String
|
public var name: String
|
||||||
|
public var shortName: String
|
||||||
public var avatarImage: UIImage?
|
public var avatarImage: UIImage?
|
||||||
public var audioOutput: AudioOutput
|
public var audioOutput: AudioOutput
|
||||||
public var isMicrophoneMuted: Bool
|
public var isMicrophoneMuted: Bool
|
||||||
@ -59,6 +107,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
public init(
|
public init(
|
||||||
lifecycleState: LifecycleState,
|
lifecycleState: LifecycleState,
|
||||||
name: String,
|
name: String,
|
||||||
|
shortName: String,
|
||||||
avatarImage: UIImage?,
|
avatarImage: UIImage?,
|
||||||
audioOutput: AudioOutput,
|
audioOutput: AudioOutput,
|
||||||
isMicrophoneMuted: Bool,
|
isMicrophoneMuted: Bool,
|
||||||
@ -67,6 +116,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
) {
|
) {
|
||||||
self.lifecycleState = lifecycleState
|
self.lifecycleState = lifecycleState
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.shortName = shortName
|
||||||
self.avatarImage = avatarImage
|
self.avatarImage = avatarImage
|
||||||
self.audioOutput = audioOutput
|
self.audioOutput = audioOutput
|
||||||
self.isMicrophoneMuted = isMicrophoneMuted
|
self.isMicrophoneMuted = isMicrophoneMuted
|
||||||
@ -81,6 +131,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
if lhs.name != rhs.name {
|
if lhs.name != rhs.name {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.shortName != rhs.shortName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.avatarImage != rhs.avatarImage {
|
if lhs.avatarImage != rhs.avatarImage {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -119,7 +172,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
private let backgroundLayer: CallBackgroundLayer
|
private let backgroundLayer: CallBackgroundLayer
|
||||||
private let overlayContentsView: UIView
|
private let overlayContentsView: UIView
|
||||||
private let buttonGroupView: ButtonGroupView
|
private let buttonGroupView: ButtonGroupView
|
||||||
|
private let blobTransformLayer: SimpleLayer
|
||||||
|
private let blobBackgroundLayer: CALayer
|
||||||
private let blobLayer: CallBlobsLayer
|
private let blobLayer: CallBlobsLayer
|
||||||
|
private let avatarTransformLayer: SimpleLayer
|
||||||
private let avatarLayer: AvatarLayer
|
private let avatarLayer: AvatarLayer
|
||||||
private let titleView: TextView
|
private let titleView: TextView
|
||||||
|
|
||||||
@ -127,9 +183,12 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
private var weakSignalView: WeakSignalView?
|
private var weakSignalView: WeakSignalView?
|
||||||
|
|
||||||
private var emojiView: KeyEmojiView?
|
private var emojiView: KeyEmojiView?
|
||||||
|
private var emojiExpandedInfoView: EmojiExpandedInfoView?
|
||||||
|
|
||||||
private var localVideoContainerView: VideoContainerView?
|
private let videoContainerBackgroundView: RoundedCornersView
|
||||||
private var remoteVideoContainerView: VideoContainerView?
|
private let overlayContentsVideoContainerBackgroundView: RoundedCornersView
|
||||||
|
|
||||||
|
private var videoContainerViews: [VideoContainerView] = []
|
||||||
|
|
||||||
private var activeRemoteVideoSource: VideoSource?
|
private var activeRemoteVideoSource: VideoSource?
|
||||||
private var waitingForFirstRemoteVideoFrameDisposable: Disposable?
|
private var waitingForFirstRemoteVideoFrameDisposable: Disposable?
|
||||||
@ -137,9 +196,15 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
private var activeLocalVideoSource: VideoSource?
|
private var activeLocalVideoSource: VideoSource?
|
||||||
private var waitingForFirstLocalVideoFrameDisposable: Disposable?
|
private var waitingForFirstLocalVideoFrameDisposable: Disposable?
|
||||||
|
|
||||||
|
private var canAnimateAudioLevel: Bool = false
|
||||||
|
private var isEmojiKeyExpanded: Bool = false
|
||||||
|
private var areControlsHidden: Bool = false
|
||||||
|
private var swapLocalAndRemoteVideo: Bool = false
|
||||||
|
|
||||||
private var processedInitialAudioLevelBump: Bool = false
|
private var processedInitialAudioLevelBump: Bool = false
|
||||||
private var audioLevelBump: Float = 0.0
|
private var audioLevelBump: Float = 0.0
|
||||||
|
|
||||||
|
private var currentAvatarAudioScale: CGFloat = 1.0
|
||||||
private var targetAudioLevel: Float = 0.0
|
private var targetAudioLevel: Float = 0.0
|
||||||
private var audioLevel: Float = 0.0
|
private var audioLevel: Float = 0.0
|
||||||
private var audioLevelUpdateSubscription: SharedDisplayLinkDriver.Link?
|
private var audioLevelUpdateSubscription: SharedDisplayLinkDriver.Link?
|
||||||
@ -158,19 +223,40 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
|
|
||||||
self.buttonGroupView = ButtonGroupView()
|
self.buttonGroupView = ButtonGroupView()
|
||||||
|
|
||||||
|
self.blobTransformLayer = SimpleLayer()
|
||||||
|
self.blobBackgroundLayer = self.backgroundLayer.externalBlurredLayer
|
||||||
self.blobLayer = CallBlobsLayer()
|
self.blobLayer = CallBlobsLayer()
|
||||||
|
self.blobBackgroundLayer.mask = self.blobTransformLayer
|
||||||
|
|
||||||
|
self.avatarTransformLayer = SimpleLayer()
|
||||||
self.avatarLayer = AvatarLayer()
|
self.avatarLayer = AvatarLayer()
|
||||||
|
|
||||||
|
self.videoContainerBackgroundView = RoundedCornersView(color: .black)
|
||||||
|
self.overlayContentsVideoContainerBackgroundView = RoundedCornersView(color: UIColor(white: 0.1, alpha: 1.0))
|
||||||
|
|
||||||
self.titleView = TextView()
|
self.titleView = TextView()
|
||||||
self.statusView = StatusView()
|
self.statusView = StatusView()
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.clipsToBounds = true
|
||||||
|
|
||||||
self.layer.addSublayer(self.backgroundLayer)
|
self.layer.addSublayer(self.backgroundLayer)
|
||||||
self.overlayContentsView.layer.addSublayer(self.backgroundLayer.blurredLayer)
|
self.overlayContentsView.layer.addSublayer(self.backgroundLayer.blurredLayer)
|
||||||
|
|
||||||
self.layer.addSublayer(self.blobLayer)
|
self.overlayContentsView.addSubview(self.overlayContentsVideoContainerBackgroundView)
|
||||||
self.layer.addSublayer(self.avatarLayer)
|
|
||||||
|
self.layer.addSublayer(self.blobBackgroundLayer)
|
||||||
|
self.blobTransformLayer.addSublayer(self.blobLayer)
|
||||||
|
|
||||||
|
self.avatarTransformLayer.addSublayer(self.avatarLayer)
|
||||||
|
self.layer.addSublayer(self.avatarTransformLayer)
|
||||||
|
|
||||||
|
/*let edgeTestLayer = EdgeTestLayer()
|
||||||
|
edgeTestLayer.frame = CGRect(origin: CGPoint(x: 20.0, y: 100.0), size: CGSize(width: 100.0, height: 100.0))
|
||||||
|
self.layer.addSublayer(edgeTestLayer)*/
|
||||||
|
|
||||||
|
self.addSubview(self.videoContainerBackgroundView)
|
||||||
|
|
||||||
self.overlayContentsView.mask = self.maskContents
|
self.overlayContentsView.mask = self.maskContents
|
||||||
self.addSubview(self.overlayContentsView)
|
self.addSubview(self.overlayContentsView)
|
||||||
@ -201,6 +287,8 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
}
|
}
|
||||||
self.audioLevelUpdateSubscription = nil
|
self.audioLevelUpdateSubscription = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init?(coder: NSCoder) {
|
public required init?(coder: NSCoder) {
|
||||||
@ -217,11 +305,21 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let emojiExpandedInfoView = self.emojiExpandedInfoView, self.isEmojiKeyExpanded {
|
||||||
|
if !result.isDescendant(of: emojiExpandedInfoView) {
|
||||||
|
return emojiExpandedInfoView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addIncomingAudioLevel(value: Float) {
|
public func addIncomingAudioLevel(value: Float) {
|
||||||
|
if self.canAnimateAudioLevel {
|
||||||
self.targetAudioLevel = value
|
self.targetAudioLevel = value
|
||||||
|
} else {
|
||||||
|
self.targetAudioLevel = 0.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attenuateAudioLevelStep() {
|
private func attenuateAudioLevelStep() {
|
||||||
@ -233,14 +331,24 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateAudioLevel() {
|
private func updateAudioLevel() {
|
||||||
if self.activeRemoteVideoSource == nil && self.activeLocalVideoSource == nil {
|
if self.canAnimateAudioLevel {
|
||||||
let additionalAvatarScale = CGFloat(max(0.0, min(self.audioLevel, 5.0)) * 0.05)
|
let additionalAvatarScale = CGFloat(max(0.0, min(self.audioLevel, 5.0)) * 0.05)
|
||||||
self.avatarLayer.transform = CATransform3DMakeScale(1.0 + additionalAvatarScale, 1.0 + additionalAvatarScale, 1.0)
|
self.currentAvatarAudioScale = 1.0 + additionalAvatarScale
|
||||||
|
self.avatarTransformLayer.transform = CATransform3DMakeScale(self.currentAvatarAudioScale, self.currentAvatarAudioScale, 1.0)
|
||||||
|
|
||||||
if let params = self.params, case .terminated = params.state.lifecycleState {
|
if let params = self.params, case .terminated = params.state.lifecycleState {
|
||||||
} else {
|
} else {
|
||||||
let blobAmplificationFactor: CGFloat = 2.0
|
let blobAmplificationFactor: CGFloat = 2.0
|
||||||
self.blobLayer.transform = CATransform3DMakeScale(1.0 + additionalAvatarScale * blobAmplificationFactor, 1.0 + additionalAvatarScale * blobAmplificationFactor, 1.0)
|
self.blobTransformLayer.transform = CATransform3DMakeScale(1.0 + additionalAvatarScale * blobAmplificationFactor, 1.0 + additionalAvatarScale * blobAmplificationFactor, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
if case .ended = recognizer.state {
|
||||||
|
if self.activeRemoteVideoSource != nil || self.activeLocalVideoSource != nil {
|
||||||
|
self.areControlsHidden = !self.areControlsHidden
|
||||||
|
self.update(transition: .spring(duration: 0.4))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -259,7 +367,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self.activeRemoteVideoSource = remoteVideo
|
self.activeRemoteVideoSource = remoteVideo
|
||||||
} else {
|
} else {
|
||||||
let firstVideoFrameSignal = Signal<Never, NoError> { subscriber in
|
let firstVideoFrameSignal = Signal<Never, NoError> { subscriber in
|
||||||
remoteVideo.updated = { [weak remoteVideo] in
|
return remoteVideo.addOnUpdated { [weak remoteVideo] in
|
||||||
guard let remoteVideo else {
|
guard let remoteVideo else {
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
return
|
return
|
||||||
@ -268,12 +376,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return EmptyDisposable
|
|
||||||
}
|
}
|
||||||
var shouldUpdate = false
|
var shouldUpdate = false
|
||||||
self.waitingForFirstRemoteVideoFrameDisposable = (firstVideoFrameSignal
|
self.waitingForFirstRemoteVideoFrameDisposable = (firstVideoFrameSignal
|
||||||
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|
|
||||||
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -297,7 +402,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self.activeLocalVideoSource = localVideo
|
self.activeLocalVideoSource = localVideo
|
||||||
} else {
|
} else {
|
||||||
let firstVideoFrameSignal = Signal<Never, NoError> { subscriber in
|
let firstVideoFrameSignal = Signal<Never, NoError> { subscriber in
|
||||||
localVideo.updated = { [weak localVideo] in
|
return localVideo.addOnUpdated { [weak localVideo] in
|
||||||
guard let localVideo else {
|
guard let localVideo else {
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
return
|
return
|
||||||
@ -306,12 +411,9 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return EmptyDisposable
|
|
||||||
}
|
}
|
||||||
var shouldUpdate = false
|
var shouldUpdate = false
|
||||||
self.waitingForFirstLocalVideoFrameDisposable = (firstVideoFrameSignal
|
self.waitingForFirstLocalVideoFrameDisposable = (firstVideoFrameSignal
|
||||||
|> timeout(4.0, queue: .mainQueue(), alternate: .complete())
|
|
||||||
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -328,6 +430,10 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.activeRemoteVideoSource == nil && self.activeLocalVideoSource == nil {
|
||||||
|
self.areControlsHidden = false
|
||||||
|
}
|
||||||
|
|
||||||
self.params = params
|
self.params = params
|
||||||
self.updateInternal(params: params, transition: transition)
|
self.updateInternal(params: params, transition: transition)
|
||||||
}
|
}
|
||||||
@ -340,36 +446,44 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateInternal(params: Params, transition: Transition) {
|
private func updateInternal(params: Params, transition: Transition) {
|
||||||
let backgroundFrame = CGRect(origin: CGPoint(), size: params.size)
|
let genericAlphaTransition: Transition
|
||||||
|
switch transition.animation {
|
||||||
let aspect: CGFloat = params.size.width / params.size.height
|
case .none:
|
||||||
let sizeNorm: CGFloat = 64.0
|
genericAlphaTransition = .immediate
|
||||||
let renderingSize = CGSize(width: floor(sizeNorm * aspect), height: sizeNorm)
|
case let .curve(duration, _):
|
||||||
let edgeSize: Int = 2
|
genericAlphaTransition = .easeInOut(duration: min(0.3, duration))
|
||||||
|
|
||||||
let primaryVideoSource: VideoSource?
|
|
||||||
let secondaryVideoSource: VideoSource?
|
|
||||||
if let activeRemoteVideoSource = self.activeRemoteVideoSource, let activeLocalVideoSource = self.activeLocalVideoSource {
|
|
||||||
primaryVideoSource = activeRemoteVideoSource
|
|
||||||
secondaryVideoSource = activeLocalVideoSource
|
|
||||||
} else if let activeRemoteVideoSource = self.activeRemoteVideoSource {
|
|
||||||
primaryVideoSource = activeRemoteVideoSource
|
|
||||||
secondaryVideoSource = nil
|
|
||||||
} else if let activeLocalVideoSource = self.activeLocalVideoSource {
|
|
||||||
primaryVideoSource = activeLocalVideoSource
|
|
||||||
secondaryVideoSource = nil
|
|
||||||
} else {
|
|
||||||
primaryVideoSource = nil
|
|
||||||
secondaryVideoSource = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let havePrimaryVideo = self.activeRemoteVideoSource != nil || self.activeLocalVideoSource != nil
|
let backgroundFrame = CGRect(origin: CGPoint(), size: params.size)
|
||||||
|
|
||||||
let visualBackgroundFrame = backgroundFrame.insetBy(dx: -CGFloat(edgeSize) / renderingSize.width * backgroundFrame.width, dy: -CGFloat(edgeSize) / renderingSize.height * backgroundFrame.height)
|
var activeVideoSources: [(VideoContainerView.Key, VideoSource)] = []
|
||||||
|
if self.swapLocalAndRemoteVideo {
|
||||||
|
if let activeLocalVideoSource = self.activeLocalVideoSource {
|
||||||
|
activeVideoSources.append((.background, activeLocalVideoSource))
|
||||||
|
}
|
||||||
|
if let activeRemoteVideoSource = self.activeRemoteVideoSource {
|
||||||
|
activeVideoSources.append((.foreground, activeRemoteVideoSource))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let activeRemoteVideoSource = self.activeRemoteVideoSource {
|
||||||
|
activeVideoSources.append((.background, activeRemoteVideoSource))
|
||||||
|
}
|
||||||
|
if let activeLocalVideoSource = self.activeLocalVideoSource {
|
||||||
|
activeVideoSources.append((.foreground, activeLocalVideoSource))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let havePrimaryVideo = !activeVideoSources.isEmpty
|
||||||
|
|
||||||
self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(renderingSize.width) + edgeSize * 2, height: Int(renderingSize.height) + edgeSize * 2))
|
let currentAreControlsHidden = havePrimaryVideo && self.areControlsHidden
|
||||||
|
|
||||||
|
let backgroundAspect: CGFloat = params.size.width / params.size.height
|
||||||
|
let backgroundSizeNorm: CGFloat = 64.0
|
||||||
|
let backgroundRenderingSize = CGSize(width: floor(backgroundSizeNorm * backgroundAspect), height: backgroundSizeNorm)
|
||||||
|
let visualBackgroundFrame = backgroundFrame
|
||||||
|
self.backgroundLayer.renderSpec = RenderLayerSpec(size: RenderSize(width: Int(backgroundRenderingSize.width), height: Int(backgroundRenderingSize.height)), edgeInset: 8)
|
||||||
transition.setFrame(layer: self.backgroundLayer, frame: visualBackgroundFrame)
|
transition.setFrame(layer: self.backgroundLayer, frame: visualBackgroundFrame)
|
||||||
transition.setFrame(layer: self.backgroundLayer.blurredLayer, frame: visualBackgroundFrame)
|
transition.setFrame(layer: self.backgroundLayer.blurredLayer, frame: visualBackgroundFrame)
|
||||||
|
transition.setFrame(layer: self.blobBackgroundLayer, frame: visualBackgroundFrame)
|
||||||
|
|
||||||
let backgroundStateIndex: Int
|
let backgroundStateIndex: Int
|
||||||
switch params.state.lifecycleState {
|
switch params.state.lifecycleState {
|
||||||
@ -427,17 +541,91 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self.speakerAction?()
|
self.speakerAction?()
|
||||||
}), at: 0)
|
}), at: 0)
|
||||||
}
|
}
|
||||||
self.buttonGroupView.update(size: params.size, buttons: buttons, transition: transition)
|
let contentBottomInset = self.buttonGroupView.update(size: params.size, insets: params.insets, controlsHidden: currentAreControlsHidden, buttons: buttons, transition: transition)
|
||||||
|
|
||||||
|
var expandedEmojiKeyRect: CGRect?
|
||||||
|
if self.isEmojiKeyExpanded {
|
||||||
|
let emojiExpandedInfoView: EmojiExpandedInfoView
|
||||||
|
var emojiExpandedInfoTransition = transition
|
||||||
|
let alphaTransition: Transition
|
||||||
|
if let current = self.emojiExpandedInfoView {
|
||||||
|
emojiExpandedInfoView = current
|
||||||
|
alphaTransition = genericAlphaTransition
|
||||||
|
} else {
|
||||||
|
emojiExpandedInfoTransition = emojiExpandedInfoTransition.withAnimation(.none)
|
||||||
|
if !genericAlphaTransition.animation.isImmediate {
|
||||||
|
alphaTransition = genericAlphaTransition.withAnimation(.curve(duration: 0.1, curve: .easeInOut))
|
||||||
|
} else {
|
||||||
|
alphaTransition = genericAlphaTransition
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiExpandedInfoView = EmojiExpandedInfoView(title: "This call is end-to-end encrypted", text: "If the emoji on \(params.state.shortName)'s screen are the same, this call is 100% secure.")
|
||||||
|
self.emojiExpandedInfoView = emojiExpandedInfoView
|
||||||
|
emojiExpandedInfoView.alpha = 0.0
|
||||||
|
Transition.immediate.setScale(view: emojiExpandedInfoView, scale: 0.5)
|
||||||
|
emojiExpandedInfoView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.1)
|
||||||
|
if let emojiView = self.emojiView {
|
||||||
|
self.insertSubview(emojiExpandedInfoView, belowSubview: emojiView)
|
||||||
|
} else {
|
||||||
|
self.addSubview(emojiExpandedInfoView)
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiExpandedInfoView.closeAction = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isEmojiKeyExpanded = false
|
||||||
|
self.update(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let emojiExpandedInfoSize = emojiExpandedInfoView.update(constrainedWidth: params.size.width - (params.insets.left + 16.0) * 2.0, transition: emojiExpandedInfoTransition)
|
||||||
|
let emojiExpandedInfoFrame = CGRect(origin: CGPoint(x: floor((params.size.width - emojiExpandedInfoSize.width) * 0.5), y: params.insets.top + 73.0), size: emojiExpandedInfoSize)
|
||||||
|
emojiExpandedInfoTransition.setPosition(view: emojiExpandedInfoView, position: CGPoint(x: emojiExpandedInfoFrame.minX + emojiExpandedInfoView.layer.anchorPoint.x * emojiExpandedInfoFrame.width, y: emojiExpandedInfoFrame.minY + emojiExpandedInfoView.layer.anchorPoint.y * emojiExpandedInfoFrame.height))
|
||||||
|
emojiExpandedInfoTransition.setBounds(view: emojiExpandedInfoView, bounds: CGRect(origin: CGPoint(), size: emojiExpandedInfoFrame.size))
|
||||||
|
|
||||||
|
alphaTransition.setAlpha(view: emojiExpandedInfoView, alpha: 1.0)
|
||||||
|
transition.setScale(view: emojiExpandedInfoView, scale: 1.0)
|
||||||
|
|
||||||
|
expandedEmojiKeyRect = emojiExpandedInfoFrame
|
||||||
|
} else {
|
||||||
|
if let emojiExpandedInfoView = self.emojiExpandedInfoView {
|
||||||
|
self.emojiExpandedInfoView = nil
|
||||||
|
|
||||||
|
let alphaTransition: Transition
|
||||||
|
if !genericAlphaTransition.animation.isImmediate {
|
||||||
|
alphaTransition = genericAlphaTransition.withAnimation(.curve(duration: 0.1, curve: .easeInOut))
|
||||||
|
} else {
|
||||||
|
alphaTransition = genericAlphaTransition
|
||||||
|
}
|
||||||
|
|
||||||
|
alphaTransition.setAlpha(view: emojiExpandedInfoView, alpha: 0.0, completion: { [weak emojiExpandedInfoView] _ in
|
||||||
|
emojiExpandedInfoView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
transition.setScale(view: emojiExpandedInfoView, scale: 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if case let .active(activeState) = params.state.lifecycleState {
|
if case let .active(activeState) = params.state.lifecycleState {
|
||||||
let emojiView: KeyEmojiView
|
let emojiView: KeyEmojiView
|
||||||
var emojiTransition = transition
|
var emojiTransition = transition
|
||||||
|
var emojiAlphaTransition = genericAlphaTransition
|
||||||
if let current = self.emojiView {
|
if let current = self.emojiView {
|
||||||
emojiView = current
|
emojiView = current
|
||||||
} else {
|
} else {
|
||||||
emojiTransition = transition.withAnimation(.none)
|
emojiTransition = transition.withAnimation(.none)
|
||||||
|
emojiAlphaTransition = genericAlphaTransition.withAnimation(.none)
|
||||||
emojiView = KeyEmojiView(emoji: activeState.emojiKey)
|
emojiView = KeyEmojiView(emoji: activeState.emojiKey)
|
||||||
self.emojiView = emojiView
|
self.emojiView = emojiView
|
||||||
|
emojiView.pressAction = { [weak self] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !self.isEmojiKeyExpanded {
|
||||||
|
self.isEmojiKeyExpanded = true
|
||||||
|
self.update(transition: .spring(duration: 0.4))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if emojiView.superview == nil {
|
if emojiView.superview == nil {
|
||||||
self.addSubview(emojiView)
|
self.addSubview(emojiView)
|
||||||
@ -445,11 +633,49 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
emojiView.animateIn()
|
emojiView.animateIn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emojiTransition.setFrame(view: emojiView, frame: CGRect(origin: CGPoint(x: params.size.width - params.insets.right - 12.0 - emojiView.size.width, y: params.insets.top + 27.0), size: emojiView.size))
|
emojiView.isUserInteractionEnabled = !self.isEmojiKeyExpanded
|
||||||
|
|
||||||
|
let emojiViewWasExpanded = emojiView.isExpanded
|
||||||
|
let emojiViewSize = emojiView.update(isExpanded: self.isEmojiKeyExpanded, transition: emojiTransition)
|
||||||
|
|
||||||
|
if self.isEmojiKeyExpanded {
|
||||||
|
let emojiViewFrame = CGRect(origin: CGPoint(x: floor((params.size.width - emojiViewSize.width) * 0.5), y: params.insets.top + 93.0), size: emojiViewSize)
|
||||||
|
|
||||||
|
if case let .curve(duration, curve) = transition.animation, let emojiViewWasExpanded, !emojiViewWasExpanded {
|
||||||
|
let distance = CGPoint(x: emojiViewFrame.midX - emojiView.center.x, y: emojiViewFrame.midY - emojiView.center.y)
|
||||||
|
let positionKeyframes = generateParabollicMotionKeyframes(from: emojiView.center, to: emojiViewFrame.center, elevation: -distance.y * 0.8, duration: duration, curve: curve, reverse: false)
|
||||||
|
emojiView.center = emojiViewFrame.center
|
||||||
|
emojiView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position", additive: false)
|
||||||
|
} else {
|
||||||
|
emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center)
|
||||||
|
}
|
||||||
|
emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size))
|
||||||
|
} else {
|
||||||
|
let emojiY: CGFloat
|
||||||
|
if currentAreControlsHidden {
|
||||||
|
emojiY = -8.0 - emojiViewSize.height
|
||||||
|
} else {
|
||||||
|
emojiY = params.insets.top + 12.0
|
||||||
|
}
|
||||||
|
let emojiViewFrame = CGRect(origin: CGPoint(x: params.size.width - params.insets.right - 12.0 - emojiViewSize.width, y: emojiY), size: emojiViewSize)
|
||||||
|
|
||||||
|
if case let .curve(duration, curve) = transition.animation, let emojiViewWasExpanded, emojiViewWasExpanded {
|
||||||
|
let distance = CGPoint(x: emojiViewFrame.midX - emojiView.center.x, y: emojiViewFrame.midY - emojiView.center.y)
|
||||||
|
let positionKeyframes = generateParabollicMotionKeyframes(from: emojiViewFrame.center, to: emojiView.center, elevation: distance.y * 0.8, duration: duration, curve: curve, reverse: true)
|
||||||
|
emojiView.center = emojiViewFrame.center
|
||||||
|
emojiView.layer.animateKeyframes(values: positionKeyframes.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position", additive: false)
|
||||||
|
} else {
|
||||||
|
emojiTransition.setPosition(view: emojiView, position: emojiViewFrame.center)
|
||||||
|
}
|
||||||
|
emojiTransition.setBounds(view: emojiView, bounds: CGRect(origin: CGPoint(), size: emojiViewFrame.size))
|
||||||
|
emojiAlphaTransition.setAlpha(view: emojiView, alpha: currentAreControlsHidden ? 0.0 : 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
emojiAlphaTransition.setAlpha(view: emojiView, alpha: 1.0)
|
||||||
} else {
|
} else {
|
||||||
if let emojiView = self.emojiView {
|
if let emojiView = self.emojiView {
|
||||||
self.emojiView = nil
|
self.emojiView = nil
|
||||||
transition.setAlpha(view: emojiView, alpha: 0.0, completion: { [weak emojiView] _ in
|
genericAlphaTransition.setAlpha(view: emojiView, alpha: 0.0, completion: { [weak emojiView] _ in
|
||||||
emojiView?.removeFromSuperview()
|
emojiView?.removeFromSuperview()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -458,144 +684,250 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
let collapsedAvatarSize: CGFloat = 136.0
|
let collapsedAvatarSize: CGFloat = 136.0
|
||||||
let blobSize: CGFloat = collapsedAvatarSize + 40.0
|
let blobSize: CGFloat = collapsedAvatarSize + 40.0
|
||||||
|
|
||||||
let collapsedAvatarFrame = CGRect(origin: CGPoint(x: floor((params.size.width - collapsedAvatarSize) * 0.5), y: 222.0), size: CGSize(width: collapsedAvatarSize, height: collapsedAvatarSize))
|
let collapsedAvatarFrame = CGRect(origin: CGPoint(x: floor((params.size.width - collapsedAvatarSize) * 0.5), y: max(params.insets.top + 8.0, floor(params.size.height * 0.49) - 39.0 - collapsedAvatarSize)), size: CGSize(width: collapsedAvatarSize, height: collapsedAvatarSize))
|
||||||
let expandedAvatarFrame = CGRect(origin: CGPoint(), size: params.size)
|
let expandedAvatarFrame = CGRect(origin: CGPoint(), size: params.size)
|
||||||
let expandedVideoFrame = CGRect(origin: CGPoint(), size: params.size)
|
let expandedVideoFrame = CGRect(origin: CGPoint(), size: params.size)
|
||||||
let avatarFrame = havePrimaryVideo ? expandedAvatarFrame : collapsedAvatarFrame
|
let avatarFrame = havePrimaryVideo ? expandedAvatarFrame : collapsedAvatarFrame
|
||||||
let avatarCornerRadius = havePrimaryVideo ? params.screenCornerRadius : collapsedAvatarSize * 0.5
|
let avatarCornerRadius = havePrimaryVideo ? params.screenCornerRadius : collapsedAvatarSize * 0.5
|
||||||
|
|
||||||
let minimizedVideoInsets = UIEdgeInsets(top: 124.0, left: 12.0, bottom: 178.0, right: 12.0)
|
var minimizedVideoInsets = UIEdgeInsets()
|
||||||
|
minimizedVideoInsets.top = params.insets.top + (currentAreControlsHidden ? 0.0 : 60.0)
|
||||||
|
minimizedVideoInsets.left = params.insets.left + 12.0
|
||||||
|
minimizedVideoInsets.right = params.insets.right + 12.0
|
||||||
|
minimizedVideoInsets.bottom = contentBottomInset + 12.0
|
||||||
|
|
||||||
if let primaryVideoSource {
|
var validVideoContainerKeys: [VideoContainerView.Key] = []
|
||||||
let remoteVideoContainerView: VideoContainerView
|
for i in 0 ..< activeVideoSources.count {
|
||||||
if let current = self.remoteVideoContainerView {
|
let (videoContainerKey, videoSource) = activeVideoSources[i]
|
||||||
remoteVideoContainerView = current
|
validVideoContainerKeys.append(videoContainerKey)
|
||||||
|
|
||||||
|
var animateIn = false
|
||||||
|
let videoContainerView: VideoContainerView
|
||||||
|
if let current = self.videoContainerViews.first(where: { $0.key == videoContainerKey }) {
|
||||||
|
videoContainerView = current
|
||||||
} else {
|
} else {
|
||||||
remoteVideoContainerView = VideoContainerView(frame: CGRect())
|
animateIn = true
|
||||||
self.remoteVideoContainerView = remoteVideoContainerView
|
videoContainerView = VideoContainerView(key: videoContainerKey)
|
||||||
self.insertSubview(remoteVideoContainerView, belowSubview: self.overlayContentsView)
|
switch videoContainerKey {
|
||||||
self.overlayContentsView.layer.addSublayer(remoteVideoContainerView.blurredContainerLayer)
|
case .foreground:
|
||||||
|
self.overlayContentsView.layer.addSublayer(videoContainerView.blurredContainerLayer)
|
||||||
|
|
||||||
remoteVideoContainerView.layer.position = self.avatarLayer.position
|
self.insertSubview(videoContainerView, belowSubview: self.overlayContentsView)
|
||||||
remoteVideoContainerView.layer.bounds = self.avatarLayer.bounds
|
self.videoContainerViews.append(videoContainerView)
|
||||||
remoteVideoContainerView.alpha = 0.0
|
case .background:
|
||||||
remoteVideoContainerView.blurredContainerLayer.position = self.avatarLayer.position
|
if !self.videoContainerViews.isEmpty {
|
||||||
remoteVideoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
|
self.overlayContentsView.layer.insertSublayer(videoContainerView.blurredContainerLayer, below: self.videoContainerViews[0].blurredContainerLayer)
|
||||||
remoteVideoContainerView.blurredContainerLayer.opacity = 0.0
|
|
||||||
remoteVideoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isMinimized: false, isAnimatingOut: false, transition: .immediate)
|
self.insertSubview(videoContainerView, belowSubview: self.videoContainerViews[0])
|
||||||
|
self.videoContainerViews.insert(videoContainerView, at: 0)
|
||||||
|
} else {
|
||||||
|
self.overlayContentsView.layer.addSublayer(videoContainerView.blurredContainerLayer)
|
||||||
|
|
||||||
|
self.insertSubview(videoContainerView, belowSubview: self.overlayContentsView)
|
||||||
|
self.videoContainerViews.append(videoContainerView)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if remoteVideoContainerView.video !== primaryVideoSource {
|
videoContainerView.pressAction = { [weak self] in
|
||||||
remoteVideoContainerView.video = primaryVideoSource
|
guard let self else {
|
||||||
}
|
|
||||||
|
|
||||||
transition.setPosition(view: remoteVideoContainerView, position: expandedVideoFrame.center)
|
|
||||||
transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
|
||||||
transition.setAlpha(view: remoteVideoContainerView, alpha: 1.0)
|
|
||||||
transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
|
|
||||||
transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
|
||||||
transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 1.0)
|
|
||||||
remoteVideoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, isMinimized: false, isAnimatingOut: false, transition: transition)
|
|
||||||
} else {
|
|
||||||
if let remoteVideoContainerView = self.remoteVideoContainerView {
|
|
||||||
remoteVideoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, isMinimized: false, isAnimatingOut: true, transition: transition)
|
|
||||||
transition.setPosition(layer: remoteVideoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
|
||||||
transition.setBounds(layer: remoteVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
|
||||||
transition.setAlpha(layer: remoteVideoContainerView.blurredContainerLayer, alpha: 0.0)
|
|
||||||
transition.setPosition(view: remoteVideoContainerView, position: avatarFrame.center)
|
|
||||||
transition.setBounds(view: remoteVideoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
|
||||||
if remoteVideoContainerView.alpha != 0.0 {
|
|
||||||
transition.setAlpha(view: remoteVideoContainerView, alpha: 0.0, completion: { [weak self, weak remoteVideoContainerView] completed in
|
|
||||||
guard let self, let remoteVideoContainerView, completed else {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
remoteVideoContainerView.removeFromSuperview()
|
self.swapLocalAndRemoteVideo = !self.swapLocalAndRemoteVideo
|
||||||
remoteVideoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
self.update(transition: .easeInOut(duration: 0.25))
|
||||||
if self.remoteVideoContainerView === remoteVideoContainerView {
|
|
||||||
self.remoteVideoContainerView = nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let secondaryVideoSource {
|
if videoContainerView.video !== videoSource {
|
||||||
let localVideoContainerView: VideoContainerView
|
videoContainerView.video = videoSource
|
||||||
if let current = self.localVideoContainerView {
|
}
|
||||||
localVideoContainerView = current
|
|
||||||
|
let videoContainerTransition = transition
|
||||||
|
if animateIn {
|
||||||
|
if i == 0 && self.videoContainerViews.count == 1 {
|
||||||
|
videoContainerView.layer.position = self.avatarTransformLayer.position
|
||||||
|
videoContainerView.layer.bounds = self.avatarTransformLayer.bounds
|
||||||
|
videoContainerView.alpha = 0.0
|
||||||
|
videoContainerView.blurredContainerLayer.position = self.avatarTransformLayer.position
|
||||||
|
videoContainerView.blurredContainerLayer.bounds = self.avatarTransformLayer.bounds
|
||||||
|
videoContainerView.blurredContainerLayer.opacity = 0.0
|
||||||
|
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: .immediate)
|
||||||
|
Transition.immediate.setScale(view: videoContainerView, scale: self.currentAvatarAudioScale)
|
||||||
|
Transition.immediate.setScale(view: self.videoContainerBackgroundView, scale: self.currentAvatarAudioScale)
|
||||||
} else {
|
} else {
|
||||||
localVideoContainerView = VideoContainerView(frame: CGRect())
|
videoContainerView.layer.position = expandedVideoFrame.center
|
||||||
self.localVideoContainerView = localVideoContainerView
|
videoContainerView.layer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size)
|
||||||
self.insertSubview(localVideoContainerView, belowSubview: self.overlayContentsView)
|
videoContainerView.alpha = 0.0
|
||||||
self.overlayContentsView.layer.addSublayer(localVideoContainerView.blurredContainerLayer)
|
videoContainerView.blurredContainerLayer.position = expandedVideoFrame.center
|
||||||
|
videoContainerView.blurredContainerLayer.bounds = CGRect(origin: CGPoint(), size: expandedVideoFrame.size)
|
||||||
localVideoContainerView.layer.position = self.avatarLayer.position
|
videoContainerView.blurredContainerLayer.opacity = 0.0
|
||||||
localVideoContainerView.layer.bounds = self.avatarLayer.bounds
|
videoContainerView.update(size: self.avatarTransformLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: i != 0, transition: .immediate)
|
||||||
localVideoContainerView.alpha = 0.0
|
}
|
||||||
localVideoContainerView.blurredContainerLayer.position = self.avatarLayer.position
|
|
||||||
localVideoContainerView.blurredContainerLayer.bounds = self.avatarLayer.bounds
|
|
||||||
localVideoContainerView.blurredContainerLayer.opacity = 0.0
|
|
||||||
localVideoContainerView.update(size: self.avatarLayer.bounds.size, insets: minimizedVideoInsets, cornerRadius: self.avatarLayer.params?.cornerRadius ?? 0.0, isMinimized: true, isAnimatingOut: false, transition: .immediate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if localVideoContainerView.video !== secondaryVideoSource {
|
videoContainerTransition.setPosition(view: videoContainerView, position: expandedVideoFrame.center)
|
||||||
localVideoContainerView.video = secondaryVideoSource
|
videoContainerTransition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
||||||
}
|
videoContainerTransition.setScale(view: videoContainerView, scale: 1.0)
|
||||||
|
videoContainerTransition.setPosition(layer: videoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
|
||||||
|
videoContainerTransition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
||||||
|
videoContainerTransition.setScale(layer: videoContainerView.blurredContainerLayer, scale: 1.0)
|
||||||
|
videoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: i != 0, isAnimatedOut: false, transition: videoContainerTransition)
|
||||||
|
|
||||||
transition.setPosition(view: localVideoContainerView, position: expandedVideoFrame.center)
|
let alphaTransition: Transition
|
||||||
transition.setBounds(view: localVideoContainerView, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
switch transition.animation {
|
||||||
transition.setAlpha(view: localVideoContainerView, alpha: 1.0)
|
case .none:
|
||||||
transition.setPosition(layer: localVideoContainerView.blurredContainerLayer, position: expandedVideoFrame.center)
|
alphaTransition = .immediate
|
||||||
transition.setBounds(layer: localVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: expandedVideoFrame.size))
|
case let .curve(duration, _):
|
||||||
transition.setAlpha(layer: localVideoContainerView.blurredContainerLayer, alpha: 1.0)
|
if animateIn {
|
||||||
localVideoContainerView.update(size: expandedVideoFrame.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, isMinimized: true, isAnimatingOut: false, transition: transition)
|
if i == 0 {
|
||||||
|
if self.videoContainerViews.count > 1 && self.videoContainerViews[1].isFillingBounds {
|
||||||
|
alphaTransition = .immediate
|
||||||
} else {
|
} else {
|
||||||
if let localVideoContainerView = self.localVideoContainerView {
|
alphaTransition = transition
|
||||||
localVideoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, isMinimized: false, isAnimatingOut: true, transition: transition)
|
}
|
||||||
transition.setPosition(layer: localVideoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
} else {
|
||||||
transition.setBounds(layer: localVideoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
alphaTransition = .easeInOut(duration: min(0.1, duration))
|
||||||
transition.setAlpha(layer: localVideoContainerView.blurredContainerLayer, alpha: 0.0)
|
}
|
||||||
transition.setPosition(view: localVideoContainerView, position: avatarFrame.center)
|
} else {
|
||||||
transition.setBounds(view: localVideoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
alphaTransition = transition
|
||||||
if localVideoContainerView.alpha != 0.0 {
|
}
|
||||||
transition.setAlpha(view: localVideoContainerView, alpha: 0.0, completion: { [weak self, weak localVideoContainerView] completed in
|
}
|
||||||
guard let self, let localVideoContainerView, completed else {
|
|
||||||
|
alphaTransition.setAlpha(view: videoContainerView, alpha: 1.0)
|
||||||
|
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var removedVideoContainerIndices: [Int] = []
|
||||||
|
for i in 0 ..< self.videoContainerViews.count {
|
||||||
|
let videoContainerView = self.videoContainerViews[i]
|
||||||
|
if !validVideoContainerKeys.contains(videoContainerView.key) {
|
||||||
|
removedVideoContainerIndices.append(i)
|
||||||
|
|
||||||
|
if self.videoContainerViews.count == 1 || (i == 0 && !havePrimaryVideo) {
|
||||||
|
let alphaTransition: Transition = genericAlphaTransition
|
||||||
|
|
||||||
|
videoContainerView.update(size: avatarFrame.size, insets: minimizedVideoInsets, cornerRadius: avatarCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: false, isAnimatedOut: true, transition: transition)
|
||||||
|
transition.setPosition(layer: videoContainerView.blurredContainerLayer, position: avatarFrame.center)
|
||||||
|
transition.setBounds(layer: videoContainerView.blurredContainerLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
|
transition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
|
||||||
|
transition.setPosition(view: videoContainerView, position: avatarFrame.center)
|
||||||
|
transition.setBounds(view: videoContainerView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
|
if videoContainerView.alpha != 0.0 {
|
||||||
|
alphaTransition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak videoContainerView] _ in
|
||||||
|
guard let videoContainerView else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
localVideoContainerView.removeFromSuperview()
|
videoContainerView.removeFromSuperview()
|
||||||
localVideoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
videoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
||||||
if self.localVideoContainerView === localVideoContainerView {
|
|
||||||
self.localVideoContainerView = nil
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
|
||||||
|
}
|
||||||
|
} else if i == 0 {
|
||||||
|
let alphaTransition = genericAlphaTransition
|
||||||
|
|
||||||
|
alphaTransition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak videoContainerView] _ in
|
||||||
|
guard let videoContainerView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
videoContainerView.removeFromSuperview()
|
||||||
|
videoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
|
||||||
|
} else {
|
||||||
|
let alphaTransition = genericAlphaTransition
|
||||||
|
|
||||||
|
alphaTransition.setAlpha(view: videoContainerView, alpha: 0.0, completion: { [weak videoContainerView] _ in
|
||||||
|
guard let videoContainerView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
videoContainerView.removeFromSuperview()
|
||||||
|
videoContainerView.blurredContainerLayer.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
alphaTransition.setAlpha(layer: videoContainerView.blurredContainerLayer, alpha: 0.0)
|
||||||
|
|
||||||
|
videoContainerView.update(size: params.size, insets: minimizedVideoInsets, cornerRadius: params.screenCornerRadius, controlsHidden: currentAreControlsHidden, isMinimized: true, isAnimatedOut: true, transition: transition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for index in removedVideoContainerIndices.reversed() {
|
||||||
|
self.videoContainerViews.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
if self.avatarLayer.image !== params.state.avatarImage {
|
if self.avatarLayer.image !== params.state.avatarImage {
|
||||||
self.avatarLayer.image = params.state.avatarImage
|
self.avatarLayer.image = params.state.avatarImage
|
||||||
}
|
}
|
||||||
transition.setPosition(layer: self.avatarLayer, position: avatarFrame.center)
|
|
||||||
|
transition.setPosition(layer: self.avatarTransformLayer, position: avatarFrame.center)
|
||||||
|
transition.setBounds(layer: self.avatarTransformLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
|
transition.setPosition(layer: self.avatarLayer, position: CGPoint(x: avatarFrame.width * 0.5, y: avatarFrame.height * 0.5))
|
||||||
|
|
||||||
|
if havePrimaryVideo != self.avatarLayer.params?.isExpanded {
|
||||||
|
if havePrimaryVideo {
|
||||||
|
self.canAnimateAudioLevel = false
|
||||||
|
self.audioLevel = 0.0
|
||||||
|
self.currentAvatarAudioScale = 1.0
|
||||||
|
transition.setScale(layer: self.avatarTransformLayer, scale: 1.0)
|
||||||
|
transition.setScale(layer: self.blobTransformLayer, scale: 1.0)
|
||||||
|
}
|
||||||
|
transition.setBounds(layer: self.avatarLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size), completion: { [weak self] completed in
|
||||||
|
guard let self, let params = self.params, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !havePrimaryVideo {
|
||||||
|
switch params.state.lifecycleState {
|
||||||
|
case .terminated:
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
self.canAnimateAudioLevel = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
transition.setBounds(layer: self.avatarLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
transition.setBounds(layer: self.avatarLayer, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
var expandedEmojiKeyOverlapsAvatar = false
|
||||||
|
if let expandedEmojiKeyRect, collapsedAvatarFrame.insetBy(dx: -40.0, dy: -40.0).intersects(expandedEmojiKeyRect) {
|
||||||
|
expandedEmojiKeyOverlapsAvatar = true
|
||||||
|
}
|
||||||
|
|
||||||
self.avatarLayer.update(size: collapsedAvatarFrame.size, isExpanded: havePrimaryVideo, cornerRadius: avatarCornerRadius, transition: transition)
|
self.avatarLayer.update(size: collapsedAvatarFrame.size, isExpanded: havePrimaryVideo, cornerRadius: avatarCornerRadius, transition: transition)
|
||||||
|
transition.setAlpha(layer: self.avatarLayer, alpha: (expandedEmojiKeyOverlapsAvatar && !havePrimaryVideo) ? 0.0 : 1.0)
|
||||||
|
transition.setScale(layer: self.avatarLayer, scale: expandedEmojiKeyOverlapsAvatar ? 0.001 : 1.0)
|
||||||
|
|
||||||
|
transition.setPosition(view: self.videoContainerBackgroundView, position: avatarFrame.center)
|
||||||
|
transition.setBounds(view: self.videoContainerBackgroundView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
|
transition.setScale(view: self.videoContainerBackgroundView, scale: 1.0)
|
||||||
|
transition.setAlpha(view: self.videoContainerBackgroundView, alpha: havePrimaryVideo ? 1.0 : 0.0)
|
||||||
|
self.videoContainerBackgroundView.update(cornerRadius: havePrimaryVideo ? params.screenCornerRadius : avatarCornerRadius, transition: transition)
|
||||||
|
|
||||||
|
transition.setPosition(view: self.overlayContentsVideoContainerBackgroundView, position: avatarFrame.center)
|
||||||
|
transition.setBounds(view: self.overlayContentsVideoContainerBackgroundView, bounds: CGRect(origin: CGPoint(), size: avatarFrame.size))
|
||||||
|
transition.setAlpha(view: self.overlayContentsVideoContainerBackgroundView, alpha: havePrimaryVideo ? 1.0 : 0.0)
|
||||||
|
self.overlayContentsVideoContainerBackgroundView.update(cornerRadius: havePrimaryVideo ? params.screenCornerRadius : avatarCornerRadius, transition: transition)
|
||||||
|
|
||||||
let blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize))
|
let blobFrame = CGRect(origin: CGPoint(x: floor(avatarFrame.midX - blobSize * 0.5), y: floor(avatarFrame.midY - blobSize * 0.5)), size: CGSize(width: blobSize, height: blobSize))
|
||||||
transition.setPosition(layer: self.blobLayer, position: CGPoint(x: blobFrame.midX, y: blobFrame.midY))
|
transition.setPosition(layer: self.blobTransformLayer, position: CGPoint(x: blobFrame.midX, y: blobFrame.midY))
|
||||||
|
transition.setBounds(layer: self.blobTransformLayer, bounds: CGRect(origin: CGPoint(), size: blobFrame.size))
|
||||||
|
transition.setPosition(layer: self.blobLayer, position: CGPoint(x: blobFrame.width * 0.5, y: blobFrame.height * 0.5))
|
||||||
transition.setBounds(layer: self.blobLayer, bounds: CGRect(origin: CGPoint(), size: blobFrame.size))
|
transition.setBounds(layer: self.blobLayer, bounds: CGRect(origin: CGPoint(), size: blobFrame.size))
|
||||||
|
|
||||||
let titleString: String
|
let titleString: String
|
||||||
switch params.state.lifecycleState {
|
switch params.state.lifecycleState {
|
||||||
case .terminated:
|
case .terminated:
|
||||||
|
self.titleView.contentMode = .center
|
||||||
titleString = "Call Ended"
|
titleString = "Call Ended"
|
||||||
if !transition.animation.isImmediate {
|
genericAlphaTransition.setScale(layer: self.blobLayer, scale: 0.3)
|
||||||
transition.withAnimation(.curve(duration: 0.3, curve: .easeInOut)).setScale(layer: self.blobLayer, scale: 0.3)
|
genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: 0.0)
|
||||||
} else {
|
self.canAnimateAudioLevel = false
|
||||||
transition.setScale(layer: self.blobLayer, scale: 0.3)
|
self.audioLevel = 0.0
|
||||||
}
|
self.currentAvatarAudioScale = 1.0
|
||||||
transition.setAlpha(layer: self.blobLayer, alpha: 0.0)
|
transition.setScale(layer: self.avatarTransformLayer, scale: 1.0)
|
||||||
|
transition.setScale(layer: self.blobTransformLayer, scale: 1.0)
|
||||||
default:
|
default:
|
||||||
|
self.titleView.contentMode = .scaleToFill
|
||||||
titleString = params.state.name
|
titleString = params.state.name
|
||||||
transition.setAlpha(layer: self.blobLayer, alpha: 1.0)
|
genericAlphaTransition.setAlpha(layer: self.blobLayer, alpha: (expandedEmojiKeyOverlapsAvatar && !havePrimaryVideo) ? 0.0 : 1.0)
|
||||||
|
transition.setScale(layer: self.blobLayer, scale: expandedEmojiKeyOverlapsAvatar ? 0.001 : 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleSize = self.titleView.update(
|
let titleSize = self.titleView.update(
|
||||||
@ -606,14 +938,6 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
constrainedWidth: params.size.width - 16.0 * 2.0,
|
constrainedWidth: params.size.width - 16.0 * 2.0,
|
||||||
transition: transition
|
transition: transition
|
||||||
)
|
)
|
||||||
let titleFrame = CGRect(
|
|
||||||
origin: CGPoint(
|
|
||||||
x: (params.size.width - titleSize.width) * 0.5,
|
|
||||||
y: !havePrimaryVideo ? collapsedAvatarFrame.maxY + 39.0 : params.insets.top + 17.0
|
|
||||||
),
|
|
||||||
size: titleSize
|
|
||||||
)
|
|
||||||
transition.setFrame(view: self.titleView, frame: titleFrame)
|
|
||||||
|
|
||||||
let statusState: StatusView.State
|
let statusState: StatusView.State
|
||||||
switch params.state.lifecycleState {
|
switch params.state.lifecycleState {
|
||||||
@ -661,6 +985,25 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let statusSize = self.statusView.update(state: statusState, transition: .immediate)
|
let statusSize = self.statusView.update(state: statusState, transition: .immediate)
|
||||||
|
|
||||||
|
let titleY: CGFloat
|
||||||
|
if currentAreControlsHidden {
|
||||||
|
titleY = -8.0 - titleSize.height - statusSize.height
|
||||||
|
} else if havePrimaryVideo {
|
||||||
|
titleY = params.insets.top + 2.0
|
||||||
|
} else {
|
||||||
|
titleY = collapsedAvatarFrame.maxY + 39.0
|
||||||
|
}
|
||||||
|
let titleFrame = CGRect(
|
||||||
|
origin: CGPoint(
|
||||||
|
x: (params.size.width - titleSize.width) * 0.5,
|
||||||
|
y: titleY
|
||||||
|
),
|
||||||
|
size: titleSize
|
||||||
|
)
|
||||||
|
transition.setFrame(view: self.titleView, frame: titleFrame)
|
||||||
|
genericAlphaTransition.setAlpha(view: self.titleView, alpha: currentAreControlsHidden ? 0.0 : 1.0)
|
||||||
|
|
||||||
let statusFrame = CGRect(
|
let statusFrame = CGRect(
|
||||||
origin: CGPoint(
|
origin: CGPoint(
|
||||||
x: (params.size.width - statusSize.width) * 0.5,
|
x: (params.size.width - statusSize.width) * 0.5,
|
||||||
@ -678,6 +1021,7 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
transition.setFrame(view: self.statusView, frame: statusFrame)
|
transition.setFrame(view: self.statusView, frame: statusFrame)
|
||||||
|
genericAlphaTransition.setAlpha(view: self.statusView, alpha: currentAreControlsHidden ? 0.0 : 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if case let .active(activeState) = params.state.lifecycleState, activeState.signalInfo.quality <= 0.2 {
|
if case let .active(activeState) = params.state.lifecycleState, activeState.signalInfo.quality <= 0.2 {
|
||||||
@ -690,7 +1034,13 @@ public final class PrivateCallScreen: OverlayMaskContainerView {
|
|||||||
self.addSubview(weakSignalView)
|
self.addSubview(weakSignalView)
|
||||||
}
|
}
|
||||||
let weakSignalSize = weakSignalView.update(constrainedSize: CGSize(width: params.size.width - 32.0, height: 100.0))
|
let weakSignalSize = weakSignalView.update(constrainedSize: CGSize(width: params.size.width - 32.0, height: 100.0))
|
||||||
let weakSignalFrame = CGRect(origin: CGPoint(x: floor((params.size.width - weakSignalSize.width) * 0.5), y: statusFrame.maxY + (havePrimaryVideo ? 12.0 : 12.0)), size: weakSignalSize)
|
let weakSignalY: CGFloat
|
||||||
|
if currentAreControlsHidden {
|
||||||
|
weakSignalY = params.insets.top + 2.0
|
||||||
|
} else {
|
||||||
|
weakSignalY = statusFrame.maxY + (havePrimaryVideo ? 12.0 : 12.0)
|
||||||
|
}
|
||||||
|
let weakSignalFrame = CGRect(origin: CGPoint(x: floor((params.size.width - weakSignalSize.width) * 0.5), y: weakSignalY), size: weakSignalSize)
|
||||||
if weakSignalView.bounds.isEmpty {
|
if weakSignalView.bounds.isEmpty {
|
||||||
weakSignalView.frame = weakSignalFrame
|
weakSignalView.frame = weakSignalFrame
|
||||||
if !transition.animation.isImmediate {
|
if !transition.animation.isImmediate {
|
||||||
|
@ -513,7 +513,8 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
|||||||
animateAsReplacement = true
|
animateAsReplacement = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let undoController = UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: component.context, file: itemFile, loop: false, title: nil, text: presentationData.strings.ChannelReactions_ToastLevelBoostRequired("\(nextCustomReactionCount)", "\(nextCustomReactionCount)").string, undoText: nil, customAction: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in return false })
|
let text = presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplate(presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplateLevel(Int32(nextCustomReactionCount)), presentationData.strings.ChannelReactions_ToastLevelBoostRequiredTemplateEmojiCount(Int32(nextCustomReactionCount))).string
|
||||||
|
let undoController = UndoOverlayController(presentationData: presentationData, content: .customEmoji(context: component.context, file: itemFile, loop: false, title: nil, text: text, undoText: nil, customAction: nil), elevatedLayout: false, position: .bottom, animateInAsReplacement: animateAsReplacement, action: { _ in return false })
|
||||||
self.currentUndoController = undoController
|
self.currentUndoController = undoController
|
||||||
self.environment?.controller()?.present(undoController, in: .current)
|
self.environment?.controller()?.present(undoController, in: .current)
|
||||||
}
|
}
|
||||||
@ -724,13 +725,12 @@ final class PeerAllowedReactionsScreenComponent: Component {
|
|||||||
self.reactionInput = reactionInput
|
self.reactionInput = reactionInput
|
||||||
}
|
}
|
||||||
|
|
||||||
//TOOD:localize
|
|
||||||
let reactionInputSize = reactionInput.update(
|
let reactionInputSize = reactionInput.update(
|
||||||
transition: animateIn ? .immediate : transition,
|
transition: animateIn ? .immediate : transition,
|
||||||
component: AnyComponent(EmojiListInputComponent(
|
component: AnyComponent(EmojiListInputComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
theme: environment.theme,
|
theme: environment.theme,
|
||||||
placeholder: "Add Reactions...",
|
placeholder: environment.strings.ChannelReactions_InputPlaceholder,
|
||||||
reactionItems: enabledReactions,
|
reactionItems: enabledReactions,
|
||||||
isInputActive: self.displayInput,
|
isInputActive: self.displayInput,
|
||||||
caretPosition: caretPosition,
|
caretPosition: caretPosition,
|
||||||
|
@ -893,6 +893,35 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
|||||||
|
|
||||||
strongSelf.maybeUpdateOverscrollAction(offset: offsetFromBottom)
|
strongSelf.maybeUpdateOverscrollAction(offset: offsetFromBottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var maxMessage: Message?
|
||||||
|
strongSelf.forEachVisibleMessageItemNode { itemNode in
|
||||||
|
if let item = itemNode.item {
|
||||||
|
var matches = false
|
||||||
|
if itemNode.frame.maxY < strongSelf.insets.top {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if itemNode.frame.minY >= strongSelf.insets.top {
|
||||||
|
matches = true
|
||||||
|
} else if itemNode.frame.minY >= strongSelf.insets.top - 100.0 {
|
||||||
|
matches = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches {
|
||||||
|
if let maxMessageValue = maxMessage {
|
||||||
|
if maxMessageValue.index < item.message.index {
|
||||||
|
maxMessage = item.message
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
maxMessage = item.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let maxMessage {
|
||||||
|
//print("read \(maxMessage.text)")
|
||||||
|
strongSelf.updateMaxVisibleReadIncomingMessageIndex(maxMessage.index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2523,7 +2552,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let messageIndex = messageIndex {
|
if let messageIndex = messageIndex {
|
||||||
self.updateMaxVisibleReadIncomingMessageIndex(messageIndex)
|
let _ = messageIndex
|
||||||
|
//self.updateMaxVisibleReadIncomingMessageIndex(messageIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let maxOverallIndex = maxOverallIndex, maxOverallIndex != self.maxVisibleMessageIndexReported {
|
if let maxOverallIndex = maxOverallIndex, maxOverallIndex != self.maxVisibleMessageIndexReported {
|
||||||
@ -3234,7 +3264,8 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let messageIndex = messageIndex {
|
if let messageIndex = messageIndex {
|
||||||
strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex)
|
let _ = messageIndex
|
||||||
|
//strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit fe91ca12ae602fb4685a87ac0955fbb37589e3cb
|
Subproject commit 8f41ea265404dea86f2444a47343993ccdc3a64e
|
Loading…
x
Reference in New Issue
Block a user