diff --git a/submodules/ContextUI/Sources/PinchController.swift b/submodules/ContextUI/Sources/PinchController.swift index 9cb033b06b..c08bd4a6b1 100644 --- a/submodules/ContextUI/Sources/PinchController.swift +++ b/submodules/ContextUI/Sources/PinchController.swift @@ -30,15 +30,18 @@ final class PinchSourceGesture: UIPinchGestureRecognizer { private let target: Target - private(set) var currentTransform: (CGFloat, CGPoint)? + private(set) var currentTransform: (CGFloat, CGPoint, CGPoint)? var began: (() -> Void)? - var updated: ((CGFloat, CGPoint) -> Void)? + var updated: ((CGFloat, CGPoint, CGPoint) -> Void)? var ended: (() -> Void)? - private var lastLocation: CGPoint? + private var initialLocation: CGPoint? + private var pinchLocation = CGPoint() private var currentOffset = CGPoint() + private var currentNumberOfTouches = 0 + init() { self.target = Target() @@ -52,11 +55,14 @@ final class PinchSourceGesture: UIPinchGestureRecognizer { override func reset() { super.reset() - self.lastLocation = nil + self.currentNumberOfTouches = 0 + self.initialLocation = nil } override func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) + + //self.currentTouches.formUnion(touches) } override func touchesEnded(_ touches: Set, with event: UIEvent) { @@ -69,40 +75,41 @@ final class PinchSourceGesture: UIPinchGestureRecognizer { override func touchesMoved(_ touches: Set, with event: UIEvent) { super.touchesMoved(touches, with: event) - - if touches.count >= 2 { - var locationSum = CGPoint() - for touch in touches { - let point = touch.location(in: self.view) - locationSum.x += point.x - locationSum.y += point.y - } - locationSum.x /= CGFloat(touches.count) - locationSum.y /= CGFloat(touches.count) - if let lastLocation = self.lastLocation { - self.currentOffset = CGPoint(x: locationSum.x - lastLocation.x, y: locationSum.y - lastLocation.y) - } else { - self.lastLocation = locationSum - self.currentOffset = CGPoint() - } - if let (scale, _) = self.currentTransform { - self.currentTransform = (scale, self.currentOffset) - self.updated?(scale, self.currentOffset) - } - } } private func gestureUpdated() { switch self.state { case .began: - self.lastLocation = nil self.currentOffset = CGPoint() - self.currentTransform = nil + + 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.currentOffset) - self.updated?(scale, self.currentOffset) + self.currentTransform = (scale, self.pinchLocation, self.currentOffset) + self.updated?(scale, self.pinchLocation, self.currentOffset) case .ended, .cancelled: self.ended?() default: @@ -152,12 +159,14 @@ public final class PinchSourceContainerNode: ASDisplayNode { } } + 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) -> Void)? + var updated: ((CGFloat, CGPoint, CGPoint) -> Void)? override public init() { self.gesture = PinchSourceGesture() @@ -187,12 +196,12 @@ public final class PinchSourceContainerNode: ASDisplayNode { strongSelf.deactivate?() } - self.gesture.updated = { [weak self] scale, offset in + self.gesture.updated = { [weak self] scale, pinchLocation, offset in guard let strongSelf = self else { return } - strongSelf.updated?(scale, offset) - strongSelf.scaleUpdated?(scale, .immediate) + strongSelf.updated?(min(scale, strongSelf.maxPinchScale), pinchLocation, offset) + strongSelf.scaleUpdated?(min(scale, strongSelf.maxPinchScale), .immediate) } } @@ -226,7 +235,13 @@ public final class PinchSourceContainerNode: ASDisplayNode { private final class PinchControllerNode: ViewControllerTracingNode { private weak var controller: PinchController? + + private var initialSourceFrame: CGRect? + + private let clippingNode: ASDisplayNode + private let sourceNode: PinchSourceContainerNode + private let getContentAreaInScreenSpace: () -> CGRect private let dimNode: ASDisplayNode @@ -235,17 +250,22 @@ private final class PinchControllerNode: ViewControllerTracingNode { private var hapticFeedback: HapticFeedback? - init(controller: PinchController, sourceNode: PinchSourceContainerNode) { + 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 + super.init() self.addSubnode(self.dimNode) + self.addSubnode(self.clippingNode) self.sourceNode.deactivate = { [weak self] in guard let strongSelf = self else { @@ -254,12 +274,22 @@ private final class PinchControllerNode: ViewControllerTracingNode { strongSelf.controller?.dismiss() } - self.sourceNode.updated = { [weak self] scale, offset in - guard let strongSelf = self else { + 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)) - strongSelf.sourceNode.contentNode.transform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1.0), offset.x / scale, offset.y / scale, 0.0) + + 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) + + strongSelf.sourceNode.contentNode.transform = transform + strongSelf.sourceNode.contentNode.position = CGPoint(x: initialSourceFrame.midX + offset.x - pinchOffset.x * (scale - 1.0), y: initialSourceFrame.midY + offset.y - pinchOffset.y * (scale - 1.0)) } } @@ -278,12 +308,21 @@ private final class PinchControllerNode: ViewControllerTracingNode { 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.contentNode.frame, from: self.sourceNode.view, to: self.view) self.sourceNode.contentNode.frame = convertedFrame - self.addSubnode(self.sourceNode.contentNode) + self.initialSourceFrame = convertedFrame + self.clippingNode.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) { @@ -298,34 +337,51 @@ private final class PinchControllerNode: ViewControllerTracingNode { completion() } - if let (scale, offset) = self.sourceNode.gesture.currentTransform { - let duration = 0.4 + 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) - Queue.mainQueue().after(0.2, { [weak self] in - guard let strongSelf = self else { - return - } - strongSelf.hapticFeedback?.impact(.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, y: offset.y), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { _ in + 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: .easeInOut) + let dimNodeTransition: ContainedViewLayoutTransition = .animated(duration: 0.3, curve: transitionCurve) dimNodeTransition.updateAlpha(node: self.dimNode, alpha: 0.0) } else { performCompletion() } } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } } public final class PinchController: ViewController, StandalonePresentableController { @@ -335,6 +391,7 @@ public final class PinchController: ViewController, StandalonePresentableControl } private let sourceNode: PinchSourceContainerNode + private let getContentAreaInScreenSpace: () -> CGRect private var wasDismissed = false @@ -342,8 +399,9 @@ public final class PinchController: ViewController, StandalonePresentableControl return self.displayNode as! PinchControllerNode } - public init(sourceNode: PinchSourceContainerNode) { + public init(sourceNode: PinchSourceContainerNode, getContentAreaInScreenSpace: @escaping () -> CGRect) { self.sourceNode = sourceNode + self.getContentAreaInScreenSpace = getContentAreaInScreenSpace super.init(navigationBarPresentationData: nil) @@ -361,7 +419,7 @@ public final class PinchController: ViewController, StandalonePresentableControl } override public func loadDisplayNode() { - self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode) + self.displayNode = PinchControllerNode(controller: self, sourceNode: self.sourceNode, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) self.displayNodeDidLoad() diff --git a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift index cde35cd894..381bab5ce5 100644 --- a/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift +++ b/submodules/InstantPageUI/Sources/InstantPageControllerNode.swift @@ -563,7 +563,13 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { guard let strongSelf = self, let controller = strongSelf.controller else { return } - let pinchController = PinchController(sourceNode: sourceNode) + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + guard let strongSelf = self, let controller = strongSelf.controller else { + return CGRect() + } + + return controller.view.convert(controller.view.bounds, to: nil) + }) controller.window?.presentInGlobalOverlay(pinchController) }, openPeer: { [weak self] peerId in self?.openPeer(peerId) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 1934261113..3193ec995f 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -916,7 +916,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard let strongSelf = self else { return } - let pinchController = PinchController(sourceNode: sourceNode) + let pinchController = PinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: { + guard let strongSelf = self else { + return CGRect() + } + + return strongSelf.chatDisplayNode.view.convert(strongSelf.chatDisplayNode.frameForVisibleArea(), to: nil) + }) strongSelf.window?.presentInGlobalOverlay(pinchController) }, openMessageContextActions: { message, node, rect, gesture in gesture?.cancel() diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index ebab0dad4c..fe7c0e93fa 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -143,7 +143,6 @@ class ChatMessageShareButton: HighlightableButtonNode { class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { private let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode - private let pinchContainerNode: PinchSourceContainerNode let imageNode: TransformImageNode private var placeholderNode: StickerShimmerEffectNode private var animationNode: GenericAnimatedStickerNode? @@ -196,7 +195,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { required init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() - self.pinchContainerNode = PinchSourceContainerNode() self.imageNode = TransformImageNode() self.dateAndStatusNode = ChatMessageDateAndStatusNode() @@ -264,8 +262,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { self.imageNode.displaysAsynchronously = false self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode - self.pinchContainerNode.contentNode.addSubnode(self.containerNode) - self.addSubnode(self.pinchContainerNode) + self.addSubnode(self.containerNode) self.contextSourceNode.contentNode.addSubnode(self.imageNode) self.contextSourceNode.contentNode.addSubnode(self.placeholderNode) self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) @@ -281,23 +278,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } item.controllerInteraction.openMessageReactions(item.message.id) } - - self.pinchContainerNode.activate = { [weak self] sourceNode in - guard let strongSelf = self, let item = strongSelf.item else { - return - } - item.controllerInteraction.activateMessagePinch(sourceNode) - } - - self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in - guard let strongSelf = self else { - return - } - - let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0)) - - transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor) - } } deinit { @@ -996,8 +976,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) - strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) - strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate) strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) @@ -1085,7 +1063,6 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { imageApply() strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame - strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect if let updatedShareButtonNode = updatedShareButtonNode { diff --git a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift index 98068bd74c..3a5ee9c215 100644 --- a/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageStickerItemNode.swift @@ -21,7 +21,6 @@ private let inlineBotNameFont = nameFont class ChatMessageStickerItemNode: ChatMessageItemView { private let contextSourceNode: ContextExtractedContentContainingNode private let containerNode: ContextControllerSourceNode - private let pinchContainerNode: PinchSourceContainerNode let imageNode: TransformImageNode private var placeholderNode: StickerShimmerEffectNode var textNode: TextNode? @@ -54,7 +53,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView { required init() { self.contextSourceNode = ContextExtractedContentContainingNode() self.containerNode = ContextControllerSourceNode() - self.pinchContainerNode = PinchSourceContainerNode() self.imageNode = TransformImageNode() self.placeholderNode = StickerShimmerEffectNode() self.placeholderNode.isUserInteractionEnabled = false @@ -121,8 +119,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { self.imageNode.displaysAsynchronously = false self.containerNode.addSubnode(self.contextSourceNode) self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode - self.pinchContainerNode.contentNode.addSubnode(self.containerNode) - self.addSubnode(self.pinchContainerNode) + self.addSubnode(self.containerNode) self.contextSourceNode.contentNode.addSubnode(self.placeholderNode) self.contextSourceNode.contentNode.addSubnode(self.imageNode) self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode) @@ -138,23 +135,6 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } item.controllerInteraction.openMessageReactions(item.message.id) } - - self.pinchContainerNode.activate = { [weak self] sourceNode in - guard let strongSelf = self, let item = strongSelf.item else { - return - } - item.controllerInteraction.activateMessagePinch(sourceNode) - } - - self.pinchContainerNode.scaleUpdated = { [weak self] scale, transition in - guard let strongSelf = self else { - return - } - - let factor: CGFloat = max(0.0, min(1.0, (scale - 1.0) * 8.0)) - - transition.updateAlpha(node: strongSelf.dateAndStatusNode, alpha: 1.0 - factor) - } } required init?(coder aDecoder: NSCoder) { @@ -675,12 +655,9 @@ class ChatMessageStickerItemNode: ChatMessageItemView { strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) - strongSelf.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: layoutSize) - strongSelf.pinchContainerNode.update(size: layoutSize, transition: .immediate) strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize) strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame - strongSelf.pinchContainerNode.contentRect = strongSelf.imageNode.frame strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect dateAndStatusApply(false)