import Foundation import UIKit import Display import AsyncDisplayKit import PagerComponent private func traceScrollView(view: UIView, point: CGPoint) -> (UIScrollView?, Bool) { for subview in view.subviews.reversed() { let subviewPoint = view.convert(point, to: subview) if subview.frame.contains(point) || subview is PagerExternalTopPanelContainer { let (result, shouldContinue) = traceScrollView(view: subview, point: subviewPoint) if let result = result { return (result, false) } else if subview.backgroundColor != nil { return (nil, false) } else if !shouldContinue { return (nil, false) } } } if let scrollView = view as? UIScrollView { return (scrollView, false) } return (nil, true) } private func traceScrollViewUp(view: UIView) -> UIScrollView? { if let scrollView = view as? UIScrollView { return scrollView } else if let superview = view.superview { return traceScrollViewUp(view: superview) } else { return nil } } private final class InternalGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if let _ = otherGestureRecognizer.view as? PagerExpandableScrollView { return true } if let _ = gestureRecognizer as? PagerPanGestureRecognizer { return true } return true } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { if let _ = otherGestureRecognizer.view as? PagerExpandableScrollView { return true } if otherGestureRecognizer is UIPanGestureRecognizer { return true } return false } } private final class ExpansionPanRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { private let internalDelegate = InternalGestureRecognizerDelegate() enum LockDirection { case up case down case any } var requiredLockDirection: LockDirection = .up private var beginPosition = CGPoint() private var currentTranslation = CGPoint() override public init(target: Any?, action: Selector?) { super.init(target: target, action: action) self.delegate = self.internalDelegate } override public func reset() { super.reset() self.state = .possible self.currentTranslation = CGPoint() } override public func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) guard let touch = touches.first, let view = self.view else { self.state = .failed return } var found = false let point = touch.location(in: self.view) let hitView = view.hitTest(point, with: event) if let _ = hitView as? UIButton { } else if let hitView = hitView, hitView.asyncdisplaykit_node is ASButtonNode { } else { if let scrollView = traceScrollView(view: view, point: point).0 ?? hitView.flatMap(traceScrollViewUp) { if scrollView is ListViewScroller || scrollView is GridNodeScrollerView || scrollView.asyncdisplaykit_node is ASScrollNode { found = false } else if let textView = scrollView as? UITextView { if textView.contentSize.height <= textView.bounds.height { found = true } else { found = false } } else { found = true } } else { found = true } } if found { self.beginPosition = point } else { self.state = .failed } } override public func touchesMoved(_ touches: Set, with event: UIEvent) { super.touchesMoved(touches, with: event) guard let touch = touches.first, let view = self.view else { self.state = .failed return } let point = touch.location(in: self.view) let translation = CGPoint(x: point.x - self.beginPosition.x, y: point.y - self.beginPosition.y) self.currentTranslation = translation if self.state == .possible { if abs(translation.x) > 8.0 { self.state = .failed return } var lockDirection: LockDirection? let point = touch.location(in: self.view) let tracedView = view.hitTest(point, with: event) if let scrollView = traceScrollView(view: view, point: point).0 { if !(scrollView is PagerExpandableScrollView) { lockDirection = .any } else { let contentOffset = scrollView.contentOffset let contentInset = scrollView.contentInset if contentOffset.y <= contentInset.top { lockDirection = .down } } } else { lockDirection = .any } if let lockDirection = lockDirection { if abs(translation.y) > 2.0 { switch lockDirection { case .up: if translation.y < 0.0 { if let tracedView = tracedView { cancelParentGestures(view: tracedView, ignore: [self]) } self.state = .began } else { self.state = .failed } case .down: if translation.y > 0.0 { if let tracedView = tracedView { cancelParentGestures(view: tracedView, ignore: [self]) } self.state = .began } else { self.state = .failed } case .any: if let tracedView = tracedView { cancelParentGestures(view: tracedView, ignore: [self]) } self.state = .began } } } else { self.state = .failed } } else { self.state = .changed } } override public func touchesEnded(_ touches: Set, with event: UIEvent) { super.touchesEnded(touches, with: event) self.state = .ended } override public func touchesCancelled(_ touches: Set, with event: UIEvent) { super.touchesCancelled(touches, with: event) self.state = .cancelled } func translation() -> CGPoint { return self.currentTranslation } func velocity() -> CGPoint { return CGPoint() } } public final class ChatInputPanelContainer: SparseNode { public var expansionUpdated: ((ContainedViewLayoutTransition) -> Void)? private var expansionRecognizer: ExpansionPanRecognizer? private var scrollableDistance: CGFloat? public private(set) var initialExpansionFraction: CGFloat = 0.0 public private(set) var expansionFraction: CGFloat = 0.0 public private(set) var stableIsExpanded: Bool = false override public init() { super.init() let expansionRecognizer = ExpansionPanRecognizer(target: self, action: #selector(self.panGesture(_:))) self.expansionRecognizer = expansionRecognizer self.view.addGestureRecognizer(expansionRecognizer) expansionRecognizer.isEnabled = false } @objc private func panGesture(_ recognizer: ExpansionPanRecognizer) { switch recognizer.state { case .began: guard let _ = self.scrollableDistance else { return } self.initialExpansionFraction = self.expansionFraction case .changed: guard let scrollableDistance = self.scrollableDistance else { return } let delta = -recognizer.translation().y / scrollableDistance self.expansionFraction = max(0.0, min(1.0, self.initialExpansionFraction + delta)) self.expansionUpdated?(.immediate) case .ended, .cancelled: guard let _ = self.scrollableDistance else { return } let velocity = recognizer.velocity() if abs(self.initialExpansionFraction - self.expansionFraction) > 0.25 { if self.initialExpansionFraction < 0.5 { self.expansionFraction = 1.0 } else { self.expansionFraction = 0.0 } } else if abs(velocity.y) > 100.0 { if velocity.y < 0.0 { self.expansionFraction = 1.0 } else { self.expansionFraction = 0.0 } } else { if self.initialExpansionFraction < 0.5 { self.expansionFraction = 0.0 } else { self.expansionFraction = 1.0 } } self.stableIsExpanded = self.expansionFraction == 1.0 if let expansionRecognizer = self.expansionRecognizer { expansionRecognizer.requiredLockDirection = self.expansionFraction == 0.0 ? .up : .down } self.expansionUpdated?(.animated(duration: 0.4, curve: .spring)) default: break } } public func update(size: CGSize, scrollableDistance: CGFloat, isExpansionEnabled: Bool, transition: ContainedViewLayoutTransition) { self.expansionRecognizer?.isEnabled = isExpansionEnabled self.scrollableDistance = scrollableDistance } public func expand() { self.expansionFraction = 1.0 self.expansionRecognizer?.requiredLockDirection = self.expansionFraction == 0.0 ? .up : .down self.stableIsExpanded = self.expansionFraction == 1.0 } public func collapse() { self.expansionFraction = 0.0 self.expansionRecognizer?.requiredLockDirection = self.expansionFraction == 0.0 ? .up : .down self.stableIsExpanded = self.expansionFraction == 1.0 } public func toggleIfEnabled() { if let expansionRecognizer = self.expansionRecognizer, expansionRecognizer.isEnabled { if self.expansionFraction == 0.0 { self.expansionFraction = 1.0 } else { self.expansionFraction = 0.0 } self.stableIsExpanded = self.expansionFraction == 1.0 self.expansionUpdated?(.animated(duration: 0.4, curve: .spring)) } } }