mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
428 lines
20 KiB
Swift
428 lines
20 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import AccountContext
|
|
|
|
private final class OverlayMediaControllerNodeView: UITracingLayerView {
|
|
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
return self.hitTestImpl?(point, event)
|
|
}
|
|
}
|
|
|
|
private final class OverlayMediaVideoNodeData {
|
|
var node: OverlayMediaItemNode
|
|
var location: CGPoint
|
|
var isMinimized: Bool
|
|
var currentSize: CGSize
|
|
|
|
init(node: OverlayMediaItemNode, location: CGPoint, isMinimized: Bool, currentSize: CGSize) {
|
|
self.node = node
|
|
self.location = location
|
|
self.isMinimized = isMinimized
|
|
self.currentSize = currentSize
|
|
}
|
|
}
|
|
|
|
|
|
|
|
final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|
private let updatePossibleEmbeddingItem: (OverlayMediaControllerEmbeddingItem?) -> Void
|
|
private let embedPossibleEmbeddingItem: (OverlayMediaControllerEmbeddingItem) -> Bool
|
|
|
|
private var videoNodes: [OverlayMediaVideoNodeData] = []
|
|
private var validLayout: ContainerViewLayout?
|
|
|
|
private var locationByGroup: [OverlayMediaItemNodeGroup: CGPoint] = [:]
|
|
|
|
private weak var draggingNode: OverlayMediaItemNode?
|
|
private var draggingStartPosition = CGPoint()
|
|
|
|
private var pinchingNode: OverlayMediaItemNode?
|
|
private var pinchingNodeInitialSize: CGSize?
|
|
|
|
init(updatePossibleEmbeddingItem: @escaping (OverlayMediaControllerEmbeddingItem?) -> Void, embedPossibleEmbeddingItem: @escaping (OverlayMediaControllerEmbeddingItem) -> Bool) {
|
|
self.updatePossibleEmbeddingItem = updatePossibleEmbeddingItem
|
|
self.embedPossibleEmbeddingItem = embedPossibleEmbeddingItem
|
|
|
|
super.init()
|
|
|
|
self.setViewBlock({
|
|
return OverlayMediaControllerNodeView()
|
|
})
|
|
|
|
(self.view as! OverlayMediaControllerNodeView).hitTestImpl = { [weak self] point, event in
|
|
return self?.hitTest(point, with: event)
|
|
}
|
|
|
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
|
panRecognizer.cancelsTouchesInView = false
|
|
panRecognizer.delegate = self
|
|
self.view.addGestureRecognizer(panRecognizer)
|
|
|
|
let pinchRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchGesture(_:)))
|
|
pinchRecognizer.cancelsTouchesInView = false
|
|
pinchRecognizer.delegate = self
|
|
self.view.addGestureRecognizer(pinchRecognizer)
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
if gestureRecognizer is UIPinchGestureRecognizer {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
for item in self.videoNodes {
|
|
if item.node.frame.contains(point) {
|
|
if let result = item.node.hitTest(point.offsetBy(dx: -item.node.frame.origin.x, dy: -item.node.frame.origin.y), with: event) {
|
|
return result
|
|
} else {
|
|
return item.node.view
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = layout
|
|
|
|
for item in self.videoNodes {
|
|
let nodeSize = item.currentSize
|
|
transition.updateFrame(node: item.node, frame: CGRect(origin: self.nodePosition(layout: layout, size: nodeSize, location: item.location, hidden: !item.node.customTransition && !item.node.hasAttachedContext, isMinimized: item.isMinimized, tempExtendedTopInset: item.node.tempExtendedTopInset), size: nodeSize))
|
|
item.node.updateLayout(nodeSize)
|
|
}
|
|
}
|
|
|
|
private func nodePosition(layout: ContainerViewLayout, size: CGSize, location: CGPoint, hidden: Bool, isMinimized: Bool, tempExtendedTopInset: Bool) -> CGPoint {
|
|
var layoutInsets = layout.insets(options: [.input])
|
|
layoutInsets.bottom += 48.0
|
|
if tempExtendedTopInset {
|
|
layoutInsets.top += 38.0
|
|
}
|
|
let inset: CGFloat = 4.0 + layout.safeInsets.left
|
|
var result = CGPoint()
|
|
if location.x.isZero {
|
|
if isMinimized {
|
|
result.x = inset - size.width + 40.0
|
|
} else if hidden {
|
|
result.x = -size.width - inset
|
|
} else {
|
|
result.x = inset
|
|
}
|
|
} else {
|
|
if isMinimized {
|
|
result.x = layout.size.width - inset - 40.0
|
|
} else if hidden {
|
|
result.x = layout.size.width + inset
|
|
} else {
|
|
result.x = layout.size.width - inset - size.width
|
|
}
|
|
}
|
|
if location.y.isZero {
|
|
result.y = layoutInsets.top + inset
|
|
} else {
|
|
result.y = layout.size.height - layoutInsets.bottom - inset - size.height
|
|
}
|
|
return result
|
|
}
|
|
|
|
private func nodeLocationForPosition(layout: ContainerViewLayout, position: CGPoint, velocity: CGPoint, size: CGSize, tempExtendedTopInset: Bool) -> (CGPoint, Bool) {
|
|
var layoutInsets = layout.insets(options: [.input])
|
|
layoutInsets.bottom += 48.0
|
|
if tempExtendedTopInset {
|
|
layoutInsets.top += 38.0
|
|
}
|
|
var result = CGPoint()
|
|
if position.x < layout.size.width / 2.0 {
|
|
result.x = 0.0
|
|
} else {
|
|
result.x = 1.0
|
|
}
|
|
if position.y < layoutInsets.top + (layout.size.height - layoutInsets.bottom - layoutInsets.top) / 2.0 {
|
|
result.y = 0.0
|
|
} else {
|
|
result.y = 1.0
|
|
}
|
|
|
|
let currentPosition = result
|
|
|
|
let angleEpsilon: CGFloat = 30.0
|
|
var shouldHide = false
|
|
|
|
if (velocity.x * velocity.x + velocity.y * velocity.y) >= 500.0 * 500.0 {
|
|
let x = velocity.x
|
|
let y = velocity.y
|
|
|
|
var angle = atan2(y, x) * 180.0 / CGFloat.pi * -1.0
|
|
if angle < 0.0 {
|
|
angle += 360.0
|
|
}
|
|
|
|
if currentPosition.x.isZero && currentPosition.y.isZero {
|
|
if ((angle > 0 && angle < 90 - angleEpsilon) || angle > 360 - angleEpsilon) {
|
|
result.x = 1.0
|
|
result.y = 0.0
|
|
} else if (angle > 180 + angleEpsilon && angle < 270 + angleEpsilon) {
|
|
result.x = 0.0
|
|
result.y = 1.0
|
|
} else if (angle > 270 + angleEpsilon && angle < 360 - angleEpsilon) {
|
|
result.x = 1.0
|
|
result.y = 1.0
|
|
} else {
|
|
shouldHide = true
|
|
}
|
|
} else if !currentPosition.x.isZero && currentPosition.y.isZero {
|
|
if (angle > 90 + angleEpsilon && angle < 180 + angleEpsilon) {
|
|
result.x = 0.0
|
|
result.y = 0.0
|
|
}
|
|
else if (angle > 270 - angleEpsilon && angle < 360 - angleEpsilon) {
|
|
result.x = 1.0
|
|
result.y = 1.0
|
|
}
|
|
else if (angle > 180 + angleEpsilon && angle < 270 - angleEpsilon) {
|
|
result.x = 0.0
|
|
result.y = 1.0
|
|
}
|
|
else {
|
|
shouldHide = true
|
|
}
|
|
} else if currentPosition.x.isZero && !currentPosition.y.isZero {
|
|
if (angle > 90 - angleEpsilon && angle < 180 - angleEpsilon) {
|
|
result.x = 0.0
|
|
result.y = 0.0
|
|
}
|
|
else if (angle < angleEpsilon || angle > 270 + angleEpsilon) {
|
|
result.x = 1.0
|
|
result.y = 1.0
|
|
}
|
|
else if (angle > angleEpsilon && angle < 90 - angleEpsilon) {
|
|
result.x = 1.0
|
|
result.y = 0.0
|
|
}
|
|
else if (!shouldHide) {
|
|
shouldHide = true
|
|
}
|
|
} else if !currentPosition.x.isZero && !currentPosition.y.isZero {
|
|
if (angle > angleEpsilon && angle < 90 + angleEpsilon) {
|
|
result.x = 1.0
|
|
result.y = 0.0
|
|
}
|
|
else if (angle > 180 - angleEpsilon && angle < 270 - angleEpsilon) {
|
|
result.x = 0.0
|
|
result.y = 1.0
|
|
}
|
|
else if (angle > 90 + angleEpsilon && angle < 180 - angleEpsilon) {
|
|
result.x = 0.0
|
|
result.y = 0.0
|
|
}
|
|
else if (!shouldHide) {
|
|
shouldHide = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return (result, shouldHide)
|
|
}
|
|
|
|
var hasNodes: Bool {
|
|
return !self.videoNodes.isEmpty
|
|
}
|
|
|
|
func addNode(_ node: OverlayMediaItemNode, customTransition: Bool) {
|
|
var location = CGPoint(x: 1.0, y: 0.0)
|
|
node.customTransition = customTransition
|
|
if let group = node.group {
|
|
if let groupLocation = self.locationByGroup[group] {
|
|
location = groupLocation
|
|
}
|
|
}
|
|
let nodeData = OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false, currentSize: node.preferredSizeForOverlayDisplay(boundingSize: self.frame.size))
|
|
self.videoNodes.append(nodeData)
|
|
self.addSubnode(node)
|
|
if let validLayout = self.validLayout {
|
|
let nodeSize = nodeData.currentSize
|
|
if self.draggingNode !== node {
|
|
if customTransition {
|
|
node.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: location, hidden: false, isMinimized: false, tempExtendedTopInset: node.tempExtendedTopInset), size: nodeSize)
|
|
} else {
|
|
node.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: location, hidden: true, isMinimized: false, tempExtendedTopInset: node.tempExtendedTopInset), size: nodeSize)
|
|
}
|
|
}
|
|
node.updateLayout(nodeSize)
|
|
|
|
self.containerLayoutUpdated(validLayout, transition: .immediate)
|
|
|
|
if !customTransition {
|
|
let positionX = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: location, hidden: true, isMinimized: false, tempExtendedTopInset: node.tempExtendedTopInset), size: nodeSize).center.x
|
|
node.layer.animatePosition(from: CGPoint(x: positionX - node.layer.position.x, y: 0.0), to: CGPoint(), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
}
|
|
}
|
|
node.hasAttachedContextUpdated = { [weak self] _ in
|
|
if let strongSelf = self, let validLayout = strongSelf.validLayout, !customTransition {
|
|
strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.3, curve: .spring))
|
|
}
|
|
}
|
|
node.unminimize = { [weak self, weak node] in
|
|
if let strongSelf = self, let node = node {
|
|
if let index = strongSelf.videoNodes.firstIndex(where: { $0.node === node }), let validLayout = strongSelf.validLayout, node !== strongSelf.draggingNode, strongSelf.videoNodes[index].isMinimized {
|
|
strongSelf.videoNodes[index].isMinimized = false
|
|
node.updateMinimizedEdge(nil, adjusting: true)
|
|
strongSelf.containerLayoutUpdated(validLayout, transition: .animated(duration: 0.3, curve: .spring))
|
|
}
|
|
}
|
|
}
|
|
node.setShouldAcquireContext(true)
|
|
}
|
|
|
|
func removeNode(_ node: OverlayMediaItemNode, customTransition: Bool) {
|
|
if node.supernode === self {
|
|
node.hasAttachedContextUpdated = nil
|
|
node.setShouldAcquireContext(false)
|
|
if let index = self.videoNodes.firstIndex(where: { $0.node === node }), let validLayout = self.validLayout {
|
|
if customTransition {
|
|
node.removeFromSupernode()
|
|
} else {
|
|
let nodeSize = self.videoNodes[index].currentSize
|
|
node.layer.animateFrame(from: node.layer.frame, to: CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: self.videoNodes[index].location, hidden: true, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: node.tempExtendedTopInset), size: nodeSize), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak node] _ in
|
|
node?.removeFromSupernode()
|
|
})
|
|
}
|
|
} else {
|
|
node.removeFromSupernode()
|
|
}
|
|
if let index = self.videoNodes.firstIndex(where: { $0.node === node }) {
|
|
self.videoNodes.remove(at: index)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .began:
|
|
if let draggingNode = self.draggingNode, let validLayout = self.validLayout, let index = self.videoNodes.firstIndex(where: { $0.node === draggingNode }){
|
|
let nodeSize = self.videoNodes[index].currentSize
|
|
let previousFrame = draggingNode.frame
|
|
draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: self.videoNodes[index].location, hidden: !draggingNode.customTransition && !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize)
|
|
draggingNode.layer.animateFrame(from: previousFrame, to: draggingNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
self.draggingNode = nil
|
|
}
|
|
loop: for item in self.videoNodes {
|
|
if item.node.frame.contains(recognizer.location(in: self.view)) {
|
|
self.draggingNode = item.node
|
|
self.draggingStartPosition = item.node.frame.origin
|
|
break loop
|
|
}
|
|
}
|
|
case .changed:
|
|
if let draggingNode = self.draggingNode, let validLayout = self.validLayout {
|
|
let translation = recognizer.translation(in: self.view)
|
|
var nodeFrame = draggingNode.frame
|
|
nodeFrame.origin = self.draggingStartPosition.offsetBy(dx: translation.x, dy: translation.y)
|
|
if nodeFrame.midX < 0.0 {
|
|
draggingNode.updateMinimizedEdge(.left, adjusting: true)
|
|
} else if nodeFrame.midX > validLayout.size.width {
|
|
draggingNode.updateMinimizedEdge(.right, adjusting: true)
|
|
} else {
|
|
draggingNode.updateMinimizedEdge(nil, adjusting: true)
|
|
}
|
|
draggingNode.frame = nodeFrame
|
|
self.updatePossibleEmbeddingItem(OverlayMediaControllerEmbeddingItem(
|
|
position: nodeFrame.center,
|
|
itemNode: draggingNode
|
|
))
|
|
}
|
|
case .ended, .cancelled:
|
|
if let draggingNode = self.draggingNode, let validLayout = self.validLayout, let index = self.videoNodes.firstIndex(where: { $0.node === draggingNode }){
|
|
let nodeSize = self.videoNodes[index].currentSize
|
|
let previousFrame = draggingNode.frame
|
|
|
|
if self.embedPossibleEmbeddingItem(OverlayMediaControllerEmbeddingItem(
|
|
position: previousFrame.center,
|
|
itemNode: draggingNode
|
|
)) {
|
|
self.draggingNode = nil
|
|
} else {
|
|
let (updatedLocation, shouldDismiss) = self.nodeLocationForPosition(layout: validLayout, position: CGPoint(x: previousFrame.midX, y: previousFrame.midY), velocity: recognizer.velocity(in: self.view), size: nodeSize, tempExtendedTopInset: draggingNode.tempExtendedTopInset)
|
|
|
|
if shouldDismiss && draggingNode.isMinimizeable {
|
|
draggingNode.updateMinimizedEdge(updatedLocation.x.isZero ? .left : .right, adjusting: false)
|
|
self.videoNodes[index].isMinimized = true
|
|
} else {
|
|
draggingNode.updateMinimizedEdge(nil, adjusting: true)
|
|
self.videoNodes[index].isMinimized = false
|
|
}
|
|
|
|
if let group = draggingNode.group {
|
|
self.locationByGroup[group] = updatedLocation
|
|
}
|
|
self.videoNodes[index].location = updatedLocation
|
|
|
|
draggingNode.frame = CGRect(origin: self.nodePosition(layout: validLayout, size: nodeSize, location: updatedLocation, hidden: !draggingNode.hasAttachedContext, isMinimized: self.videoNodes[index].isMinimized, tempExtendedTopInset: draggingNode.tempExtendedTopInset), size: nodeSize)
|
|
draggingNode.layer.animateFrame(from: previousFrame, to: draggingNode.frame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
|
self.draggingNode = nil
|
|
|
|
if shouldDismiss && !draggingNode.isMinimizeable {
|
|
draggingNode.dismiss()
|
|
}
|
|
}
|
|
self.updatePossibleEmbeddingItem(nil)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
@objc func pinchGesture(_ recognizer: UIPinchGestureRecognizer) {
|
|
switch recognizer.state {
|
|
case .began:
|
|
let location = recognizer.location(in: self.view)
|
|
loop: for videoNode in self.videoNodes {
|
|
if videoNode.node.frame.contains(location) {
|
|
if videoNode.node.isMinimizeable {
|
|
self.pinchingNode = videoNode.node
|
|
self.pinchingNodeInitialSize = videoNode.currentSize
|
|
}
|
|
break loop
|
|
}
|
|
}
|
|
case .changed:
|
|
if let validLayout = self.validLayout, let pinchingNode = self.pinchingNode, let initialSize = self.pinchingNodeInitialSize {
|
|
let minSize = CGSize(width: 180.0, height: 90.0)
|
|
let maxSize = CGSize(width: validLayout.size.width - validLayout.safeInsets.left - validLayout.safeInsets.right - 14.0, height: 500.0)
|
|
|
|
let scale = recognizer.scale
|
|
var updatedSize = CGSize(width: floor(initialSize.width * scale), height: floor(initialSize.height * scale))
|
|
updatedSize = updatedSize.fitted(maxSize)
|
|
if updatedSize.width < minSize.width {
|
|
updatedSize = updatedSize.aspectFitted(CGSize(width: minSize.width, height: 1000.0))
|
|
}
|
|
|
|
loop: for videoNode in self.videoNodes {
|
|
if videoNode.node === pinchingNode {
|
|
videoNode.currentSize = updatedSize
|
|
break loop
|
|
}
|
|
}
|
|
|
|
self.containerLayoutUpdated(validLayout, transition: .immediate)
|
|
}
|
|
case .ended, .cancelled:
|
|
self.pinchingNode = nil
|
|
self.pinchingNodeInitialSize = nil
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|