Swiftgram/submodules/ContextUI/Sources/PeekControllerGestureRecognizer.swift
2024-03-09 03:19:31 +04:00

322 lines
14 KiB
Swift

import Foundation
import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
private func traceDeceleratingScrollView(_ view: UIView, at point: CGPoint) -> Bool {
if view.bounds.contains(point), let view = view as? UIScrollView, view.isDecelerating {
return true
}
for subview in view.subviews {
let subviewPoint = view.convert(point, to: subview)
if traceDeceleratingScrollView(subview, at: subviewPoint) {
return true
}
}
return false
}
public final class PeekControllerGestureRecognizer: UIPanGestureRecognizer {
private let contentAtPoint: (CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>?
private let present: (PeekControllerContent, UIView, CGRect) -> ViewController?
private let updateContent: (PeekControllerContent?) -> Void
private let activateBySingleTap: Bool
public var longPressEnabled = true
public var checkSingleTapActivationAtPoint: ((CGPoint) -> Bool)?
private var tapLocation: CGPoint?
private var longTapTimer: SwiftSignalKit.Timer?
private var pressTimer: SwiftSignalKit.Timer?
private let candidateContentDisposable = MetaDisposable()
private var candidateContent: (UIView, CGRect, PeekControllerContent)? {
didSet {
self.updateContent(self.candidateContent?.2)
}
}
private var menuActivation: PeerControllerMenuActivation?
private weak var presentedController: PeekController?
public init(contentAtPoint: @escaping (CGPoint) -> Signal<(UIView, CGRect, PeekControllerContent)?, NoError>?, present: @escaping (PeekControllerContent, UIView, CGRect) -> ViewController?, updateContent: @escaping (PeekControllerContent?) -> Void = { _ in }, activateBySingleTap: Bool = false) {
self.contentAtPoint = contentAtPoint
self.present = present
self.updateContent = updateContent
self.activateBySingleTap = activateBySingleTap
super.init(target: nil, action: nil)
}
deinit {
self.longTapTimer?.invalidate()
self.pressTimer?.invalidate()
self.candidateContentDisposable.dispose()
}
private func startLongTapTimer() {
guard self.longPressEnabled else {
return
}
self.longTapTimer?.invalidate()
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.4, repeat: false, completion: { [weak self] in
self?.longTapTimerFired()
}, queue: Queue.mainQueue())
self.longTapTimer = longTapTimer
longTapTimer.start()
}
private func startPressTimer() {
self.pressTimer?.invalidate()
let pressTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in
self?.pressTimerFired()
}, queue: Queue.mainQueue())
self.pressTimer = pressTimer
pressTimer.start()
}
private func stopLongTapTimer() {
self.longTapTimer?.invalidate()
self.longTapTimer = nil
}
private func stopPressTimer() {
self.pressTimer?.invalidate()
self.pressTimer = nil
}
override public func reset() {
super.reset()
self.stopLongTapTimer()
self.stopPressTimer()
self.tapLocation = nil
self.candidateContent = nil
self.menuActivation = nil
self.presentedController = nil
}
private func longTapTimerFired() {
guard let tapLocation = self.tapLocation else {
return
}
self.checkCandidateContent(at: tapLocation)
}
private func pressTimerFired() {
if let _ = self.tapLocation, let menuActivation = self.menuActivation, case .press = menuActivation {
if let presentedController = self.presentedController {
if presentedController.isNodeLoaded {
(presentedController.displayNode as? PeekControllerNode)?.activateMenu()
}
self.menuActivation = nil
// self.presentedController = nil
// self.state = .ended
}
}
}
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if let view = self.view, let tapLocation = touches.first?.location(in: view) {
if traceDeceleratingScrollView(view, at: tapLocation) {
self.candidateContent = nil
self.state = .failed
} else {
self.tapLocation = tapLocation
self.startLongTapTimer()
}
}
}
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
var activateBySingleTap = self.activateBySingleTap
if !activateBySingleTap, let checkSingleTapActivationAtPoint = self.checkSingleTapActivationAtPoint, let tapLocation = self.tapLocation {
activateBySingleTap = checkSingleTapActivationAtPoint(tapLocation)
}
if activateBySingleTap, self.presentedController == nil {
self.longTapTimer?.invalidate()
self.pressTimer?.invalidate()
if let tapLocation = self.tapLocation {
self.checkCandidateContent(at: tapLocation, forceActivate: true)
}
self.state = .ended
} else {
if let presentedController = self.presentedController, presentedController.isNodeLoaded, let location = touches.first?.location(in: presentedController.view) {
(presentedController.displayNode as? PeekControllerNode)?.endDragging(location)
self.presentedController = nil
self.menuActivation = nil
}
self.tapLocation = nil
self.candidateContent = nil
self.longTapTimer?.invalidate()
self.pressTimer?.invalidate()
self.candidateContentDisposable.set(nil)
self.state = .failed
}
}
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesCancelled(touches, with: event)
self.tapLocation = nil
self.candidateContent = nil
self.state = .failed
if let presentedController = self.presentedController {
self.menuActivation = nil
self.presentedController = nil
presentedController.dismiss()
}
}
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if let touch = touches.first, let initialTapLocation = self.tapLocation {
let touchLocation = touch.location(in: self.view)
if let presentedController = self.presentedController, self.menuActivation == nil {
if presentedController.isNodeLoaded {
let touchLocation = touch.location(in: presentedController.view)
(presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(touchLocation)
}
} else if let menuActivation = self.menuActivation, let presentedController = self.presentedController {
switch menuActivation {
case .drag:
var offset = touchLocation.y - initialTapLocation.y
let delta = abs(offset)
let factor: CGFloat = 60.0
offset = (-((1.0 - (1.0 / (((delta) * 0.55 / (factor)) + 1.0))) * factor)) * (offset < 0.0 ? 1.0 : -1.0)
if presentedController.isNodeLoaded {
// (presentedController.displayNode as? PeekControllerNode)?.applyDraggingOffset(offset)
}
case .press:
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
if touch.force >= 2.5 {
if presentedController.isNodeLoaded {
(presentedController.displayNode as? PeekControllerNode)?.activateMenu()
self.menuActivation = nil
self.presentedController = nil
self.candidateContent = nil
self.state = .ended
self.candidateContentDisposable.set(nil)
return
}
}
}
if self.pressTimer != nil {
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.startPressTimer()
}
}
if self.presentedController != nil {
self.checkCandidateContent(at: touchLocation)
}
}
} else {
let dX = touchLocation.x - initialTapLocation.x
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.stopLongTapTimer()
self.tapLocation = nil
self.candidateContent = nil
self.state = .failed
}
}
}
}
private func checkCandidateContent(at touchLocation: CGPoint, forceActivate: Bool = false) {
//print("check begin")
if let contentSignal = self.contentAtPoint(touchLocation) {
self.candidateContentDisposable.set((contentSignal
|> deliverOnMainQueue).start(next: { [weak self] result in
if let strongSelf = self {
let processResult: Bool
if forceActivate {
processResult = true
} else {
switch strongSelf.state {
case .possible, .changed:
processResult = true
default:
processResult = false
}
}
//print("check received, will process: \(processResult), force: \(forceActivate), state: \(strongSelf.state)")
if processResult {
if let (sourceView, sourceRect, content) = result {
if let currentContent = strongSelf.candidateContent {
if !currentContent.2.isEqual(to: content) {
strongSelf.tapLocation = touchLocation
strongSelf.candidateContent = (sourceView, sourceRect, content)
strongSelf.menuActivation = content.menuActivation()
if let presentedController = strongSelf.presentedController, presentedController.isNodeLoaded {
presentedController.sourceView = {
return (sourceView, sourceRect)
}
(presentedController.displayNode as? PeekControllerNode)?.updateContent(content: content)
}
}
} else {
if let presentedController = strongSelf.present(content, sourceView, sourceRect) {
if let presentedController = presentedController as? PeekController {
if forceActivate {
strongSelf.candidateContent = nil
if case .press = content.menuActivation() {
(presentedController.displayNode as? PeekControllerNode)?.activateMenu()
}
} else {
strongSelf.candidateContent = (sourceView, sourceRect, content)
strongSelf.menuActivation = content.menuActivation()
strongSelf.presentedController = presentedController
strongSelf.state = .began
switch content.menuActivation() {
case .drag:
break
case .press:
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
if presentedController.traitCollection.forceTouchCapability != .available {
strongSelf.startPressTimer()
}
} else {
strongSelf.startPressTimer()
}
}
}
} else {
if strongSelf.state != .ended {
strongSelf.state = .ended
}
}
}
}
} else if strongSelf.presentedController == nil {
if strongSelf.state != .possible && strongSelf.state != .ended {
strongSelf.state = .failed
}
}
}
}
}))
} else if self.presentedController == nil {
self.state = .failed
}
}
}