Swiftgram/submodules/ContextUI/Sources/PinchController.swift
2021-04-09 19:06:28 +04:00

485 lines
17 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramPresentationData
import TextSelectionNode
import ReactionSelectionNode
import TelegramCore
import SyncCore
import SwiftSignalKit
private func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect {
let sourceWindowFrame = fromView.convert(frame, to: nil)
var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil)
if let fromWindow = fromView.window, let toWindow = toView.window {
targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width
}
return targetWindowFrame
}
final class PinchSourceGesture: UIPinchGestureRecognizer {
private final class Target {
var updated: (() -> Void)?
@objc func onGesture(_ gesture: UIPinchGestureRecognizer) {
self.updated?()
}
}
private let target: Target
private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)?
var began: (() -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
var ended: (() -> Void)?
private var initialLocation: CGPoint?
private var pinchLocation = CGPoint()
private var currentOffset = CGPoint()
private var currentNumberOfTouches = 0
init() {
self.target = Target()
super.init(target: self.target, action: #selector(self.target.onGesture(_:)))
self.target.updated = { [weak self] in
self?.gestureUpdated()
}
}
override func reset() {
super.reset()
self.currentNumberOfTouches = 0
self.initialLocation = nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
//self.currentTouches.formUnion(touches)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
}
private func gestureUpdated() {
switch self.state {
case .began:
self.currentOffset = CGPoint()
let pinchLocation = self.location(in: self.view)
self.pinchLocation = pinchLocation
self.initialLocation = pinchLocation
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.currentNumberOfTouches = self.numberOfTouches
self.began?()
case .changed:
let locationSum = self.location(in: self.view)
if self.numberOfTouches < 2 && self.currentNumberOfTouches >= 2 {
self.initialLocation = CGPoint(x: locationSum.x - self.currentOffset.x, y: locationSum.y - self.currentOffset.y)
}
self.currentNumberOfTouches = self.numberOfTouches
if let initialLocation = self.initialLocation {
self.currentOffset = CGPoint(x: locationSum.x - initialLocation.x, y: locationSum.y - initialLocation.y)
}
if let (scale, pinchLocation, _) = self.currentTransform {
self.currentTransform = (scale, pinchLocation, self.currentOffset)
self.updated?(scale, pinchLocation, self.currentOffset)
}
let scale = max(1.0, self.scale)
self.currentTransform = (scale, self.pinchLocation, self.currentOffset)
self.updated?(scale, self.pinchLocation, self.currentOffset)
case .ended, .cancelled:
self.ended?()
default:
break
}
}
}
private func cancelContextGestures(node: ASDisplayNode) {
if let node = node as? ContextControllerSourceNode {
node.cancelGesture()
}
if let supernode = node.supernode {
cancelContextGestures(node: supernode)
}
}
private func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for recognizer in gestureRecognizers {
if let recognizer = recognizer as? InteractiveTransitionGestureRecognizer {
recognizer.cancel()
} else if let recognizer = recognizer as? WindowPanRecognizer {
recognizer.cancel()
}
}
}
if let superview = view.superview {
cancelContextGestures(view: superview)
}
}
public final class PinchSourceContainerNode: ASDisplayNode, UIGestureRecognizerDelegate {
public let contentNode: ASDisplayNode
public var contentRect: CGRect = CGRect()
private(set) var naturalContentFrame: CGRect?
fileprivate let gesture: PinchSourceGesture
fileprivate var panGesture: UIPanGestureRecognizer?
public var isPinchGestureEnabled: Bool = false {
didSet {
if self.isPinchGestureEnabled != oldValue {
self.gesture.isEnabled = self.isPinchGestureEnabled
}
}
}
public var maxPinchScale: CGFloat = 10.0
private var isActive: Bool = false
public var activate: ((PinchSourceContainerNode) -> Void)?
public var scaleUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var deactivate: (() -> Void)?
var updated: ((CGFloat, CGPoint, CGPoint) -> Void)?
override public init() {
self.gesture = PinchSourceGesture()
self.contentNode = ASDisplayNode()
super.init()
self.addSubnode(self.contentNode)
self.gesture.began = { [weak self] in
guard let strongSelf = self else {
return
}
cancelContextGestures(node: strongSelf)
cancelContextGestures(view: strongSelf.view)
strongSelf.isActive = true
strongSelf.activate?(strongSelf)
}
self.gesture.ended = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isActive = false
strongSelf.deactivate?()
}
self.gesture.updated = { [weak self] scale, pinchLocation, offset in
guard let strongSelf = self else {
return
}
strongSelf.updated?(min(scale, strongSelf.maxPinchScale), pinchLocation, offset)
strongSelf.scaleUpdated?(min(scale, strongSelf.maxPinchScale), .immediate)
}
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(self.gesture)
self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
guard let strongSelf = self else {
return false
}
return strongSelf.isActive
}
}
@objc private func panGestureRecognized(_ recognizer: UIPanGestureRecognizer) {
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
public func update(size: CGSize, transition: ContainedViewLayoutTransition) {
let contentFrame = CGRect(origin: CGPoint(), size: size)
self.naturalContentFrame = contentFrame
if !self.isActive {
transition.updateFrame(node: self.contentNode, frame: contentFrame)
}
}
func restoreToNaturalSize() {
guard let naturalContentFrame = self.naturalContentFrame else {
return
}
self.contentNode.frame = naturalContentFrame
}
}
private final class PinchControllerNode: ViewControllerTracingNode {
private weak var controller: PinchController?
private var initialSourceFrame: CGRect?
private let clippingNode: ASDisplayNode
private let scrollingContainer: ASDisplayNode
private let sourceNode: PinchSourceContainerNode
private let getContentAreaInScreenSpace: () -> CGRect
private let dimNode: ASDisplayNode
private var validLayout: ContainerViewLayout?
private var isAnimatingOut: Bool = false
private var hapticFeedback: HapticFeedback?
init(controller: PinchController, sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) {
self.controller = controller
self.sourceNode = sourceNode
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
self.dimNode = ASDisplayNode()
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.dimNode.alpha = 0.0
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
self.scrollingContainer = ASDisplayNode()
super.init()
self.addSubnode(self.dimNode)
self.addSubnode(self.clippingNode)
self.clippingNode.addSubnode(self.scrollingContainer)
self.sourceNode.deactivate = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.controller?.dismiss()
}
self.sourceNode.updated = { [weak self] scale, pinchLocation, offset in
guard let strongSelf = self, let initialSourceFrame = strongSelf.initialSourceFrame else {
return
}
strongSelf.dimNode.alpha = max(0.0, min(1.0, scale - 1.0))
let pinchOffset = CGPoint(
x: pinchLocation.x - initialSourceFrame.width / 2.0,
y: pinchLocation.y - initialSourceFrame.height / 2.0
)
var transform = CATransform3DIdentity
transform = CATransform3DTranslate(transform, offset.x - pinchOffset.x * (scale - 1.0), offset.y - pinchOffset.y * (scale - 1.0), 0.0)
transform = CATransform3DScale(transform, scale, scale, 0.0)
strongSelf.sourceNode.contentNode.transform = transform
}
}
deinit {
}
override func didLoad() {
super.didLoad()
}
func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?) {
if self.isAnimatingOut {
return
}
self.validLayout = layout
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size))
}
func animateIn() {
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
self.sourceNode.contentNode.frame = convertedFrame
self.initialSourceFrame = convertedFrame
self.scrollingContainer.addSubnode(self.sourceNode.contentNode)
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
updatedContentAreaInScreenSpace.origin.x = 0.0
updatedContentAreaInScreenSpace.size.width = self.bounds.width
self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * 1.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
func animateOut(completion: @escaping () -> Void) {
self.isAnimatingOut = true
let performCompletion: () -> Void = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.isAnimatingOut = false
strongSelf.sourceNode.restoreToNaturalSize()
strongSelf.sourceNode.addSubnode(strongSelf.sourceNode.contentNode)
completion()
}
let convertedFrame = convertFrame(self.sourceNode.bounds, from: self.sourceNode.view, to: self.view)
self.sourceNode.contentNode.frame = convertedFrame
self.initialSourceFrame = convertedFrame
if let (scale, pinchLocation, offset) = self.sourceNode.gesture.currentTransform, let initialSourceFrame = self.initialSourceFrame {
let duration = 0.3
let transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut
var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace()
updatedContentAreaInScreenSpace.origin.x = 0.0
updatedContentAreaInScreenSpace.size.width = self.bounds.width
self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: duration * 1.0, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false)
let transition: ContainedViewLayoutTransition = .animated(duration: duration, curve: .spring)
if self.hapticFeedback == nil {
self.hapticFeedback = HapticFeedback()
}
self.hapticFeedback?.prepareImpact(.light)
self.hapticFeedback?.impact(.light)
self.sourceNode.scaleUpdated?(1.0, transition)
let pinchOffset = CGPoint(
x: pinchLocation.x - initialSourceFrame.width / 2.0,
y: pinchLocation.y - initialSourceFrame.height / 2.0
)
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, scale, scale, 0.0)
self.sourceNode.contentNode.transform = CATransform3DIdentity
self.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX, y: initialSourceFrame.midY)
self.sourceNode.contentNode.layer.animateSpring(from: scale as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration * 1.2, damping: 110.0)
self.sourceNode.contentNode.layer.animatePosition(from: CGPoint(x: offset.x - pinchOffset.x * (scale - 1.0), y: offset.y - pinchOffset.y * (scale - 1.0)), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in
performCompletion()
})
let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: transitionCurve)
dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0)
} else {
performCompletion()
}
}
func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
if self.isAnimatingOut {
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset.y)
transition.animateOffsetAdditive(node: self.scrollingContainer, offset: -offset.y)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return nil
}
}
public final class PinchController: ViewController, StandalonePresentableController {
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private let sourceNode: PinchSourceContainerNode
private let getContentAreaInScreenSpace: () -> CGRect
private var wasDismissed = false
private var controllerNode: PinchControllerNode {
return self.displayNode as! PinchControllerNode
}
public init(sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) {
self.sourceNode = sourceNode
self.getContentAreaInScreenSpace = getContentAreaInScreenSpace
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.lockOrientation = true
self.blocksBackgroundWhenInOverlay = true
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
}
override public func loadDisplayNode() {
self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace)
self.displayNodeDidLoad()
self._ready.set(.single(true))
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil)
}
override public func viewDidAppear(_ animated: Bool) {
if self.ignoreAppearanceMethodInvocations() {
return
}
super.viewDidAppear(animated)
self.controllerNode.animateIn()
}
override public func dismiss(completion: (() -> Void)? = nil) {
if !self.wasDismissed {
self.wasDismissed = true
self.controllerNode.animateOut(completion: { [weak self] in
self?.presentingViewController?.dismiss(animated: false, completion: nil)
completion?()
})
}
}
public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) {
self.controllerNode.addRelativeContentOffset(offset, transition: transition)
}
}