mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 06:35:51 +00:00
Animation experiment
This commit is contained in:
191
submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift
Normal file
191
submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift
Normal file
@@ -0,0 +1,191 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import ContextUI
|
||||
|
||||
final class ChatMessageTransitionNode: ASDisplayNode {
|
||||
final class ReplyPanel {
|
||||
let titleNode: ASDisplayNode
|
||||
let textNode: ASDisplayNode
|
||||
let lineNode: ASDisplayNode
|
||||
let imageNode: ASDisplayNode
|
||||
let relativeSourceRect: CGRect
|
||||
|
||||
init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect) {
|
||||
self.titleNode = titleNode
|
||||
self.textNode = textNode
|
||||
self.lineNode = lineNode
|
||||
self.imageNode = imageNode
|
||||
self.relativeSourceRect = relativeSourceRect
|
||||
}
|
||||
}
|
||||
|
||||
enum Source {
|
||||
final class TextInput {
|
||||
let backgroundView: UIView
|
||||
let contentView: UIView
|
||||
let sourceRect: CGRect
|
||||
|
||||
init(backgroundView: UIView, contentView: UIView, sourceRect: CGRect) {
|
||||
self.backgroundView = backgroundView
|
||||
self.contentView = contentView
|
||||
self.sourceRect = sourceRect
|
||||
}
|
||||
}
|
||||
|
||||
case textInput(textInput: TextInput, replyPanel: ReplyAccessoryPanelNode?)
|
||||
}
|
||||
|
||||
private final class AnimatingItemNode: ASDisplayNode {
|
||||
private let itemNode: ChatMessageItemView
|
||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||
private let source: ChatMessageTransitionNode.Source
|
||||
|
||||
private let scrollingContainer: ASDisplayNode
|
||||
private let containerNode: ASDisplayNode
|
||||
|
||||
var animationEnded: (() -> Void)?
|
||||
|
||||
init(itemNode: ChatMessageItemView, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNode.Source) {
|
||||
self.itemNode = itemNode
|
||||
self.scrollingContainer = ASDisplayNode()
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.contextSourceNode = contextSourceNode
|
||||
self.source = source
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.scrollingContainer)
|
||||
self.scrollingContainer.addSubnode(self.containerNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.contextSourceNode.addSubnode(self.contextSourceNode.contentNode)
|
||||
}
|
||||
|
||||
func beginAnimation() {
|
||||
switch self.source {
|
||||
case let .textInput(textInput, replyPanel):
|
||||
self.containerNode.addSubnode(self.contextSourceNode.contentNode)
|
||||
|
||||
let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: nil)
|
||||
let sourceAbsoluteRect = textInput.backgroundView.frame.offsetBy(dx: textInput.sourceRect.minX, dy: textInput.sourceRect.minY)
|
||||
|
||||
var sourceReplyPanel: ReplyPanel?
|
||||
if let replyPanel = replyPanel, let replyPanelParentView = replyPanel.view.superview {
|
||||
var replySourceAbsoluteFrame = replyPanelParentView.convert(replyPanel.originalFrameBeforeDismissed ?? replyPanel.frame, to: nil)
|
||||
replySourceAbsoluteFrame.origin.x -= sourceAbsoluteRect.minX - self.contextSourceNode.contentRect.minX
|
||||
replySourceAbsoluteFrame.origin.y -= sourceAbsoluteRect.minY - self.contextSourceNode.contentRect.minY
|
||||
|
||||
sourceReplyPanel = ReplyPanel(titleNode: replyPanel.titleNode, textNode: replyPanel.textNode, lineNode: replyPanel.lineNode, imageNode: replyPanel.imageNode, relativeSourceRect: replySourceAbsoluteFrame)
|
||||
}
|
||||
|
||||
self.itemNode.cancelInsertionAnimations()
|
||||
|
||||
let duration: Double = 0.4
|
||||
let delay: Double = 0.0
|
||||
|
||||
let transition: ContainedViewLayoutTransition = .animated(duration: duration * 0.8, curve: .spring)
|
||||
|
||||
if let itemNode = self.itemNode as? ChatMessageBubbleItemNode {
|
||||
itemNode.animateContentFromTextInputField(textInput: textInput, transition: transition)
|
||||
if let sourceReplyPanel = sourceReplyPanel {
|
||||
itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: self.contextSourceNode.contentRect.minY)
|
||||
self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.minY - targetAbsoluteRect.minY), to: CGPoint(), duration: duration, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true, force: true, completion: { [weak self] _ in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.endAnimation()
|
||||
})
|
||||
self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), to: CGPoint(), duration: duration * 0.8, delay: delay, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func endAnimation() {
|
||||
self.animationEnded?()
|
||||
}
|
||||
|
||||
func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) {
|
||||
var applyOffset = false
|
||||
if let itemNode = itemNode {
|
||||
if itemNode === self.itemNode {
|
||||
applyOffset = true
|
||||
}
|
||||
} else {
|
||||
applyOffset = true
|
||||
}
|
||||
if applyOffset {
|
||||
self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset)
|
||||
transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let listNode: ChatHistoryListNode
|
||||
|
||||
private var currentPendingItem: (Int64, Source, () -> Void)?
|
||||
|
||||
private var animatingItemNodes: [AnimatingItemNode] = []
|
||||
|
||||
init(listNode: ChatHistoryListNode) {
|
||||
self.listNode = listNode
|
||||
|
||||
super.init()
|
||||
|
||||
self.listNode.animationCorrelationMessageFound = { [weak self] itemNode, correlationId in
|
||||
guard let strongSelf = self, let (currentId, currentSource, initiated) = strongSelf.currentPendingItem else {
|
||||
return
|
||||
}
|
||||
if currentId == correlationId {
|
||||
strongSelf.currentPendingItem = nil
|
||||
strongSelf.beginAnimation(itemNode: itemNode, source: currentSource)
|
||||
initiated()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func add(correlationId: Int64, source: Source, initiated: @escaping () -> Void) {
|
||||
self.currentPendingItem = (correlationId, source, initiated)
|
||||
self.listNode.setCurrentSendAnimationCorrelationId(correlationId)
|
||||
}
|
||||
|
||||
private func beginAnimation(itemNode: ChatMessageItemView, source: Source) {
|
||||
if let itemNode = itemNode as? ChatMessageBubbleItemNode {
|
||||
let animatingItemNode = AnimatingItemNode(itemNode: itemNode, contextSourceNode: itemNode.mainContextSourceNode, source: source)
|
||||
self.animatingItemNodes.append(animatingItemNode)
|
||||
self.addSubnode(animatingItemNode)
|
||||
|
||||
animatingItemNode.animationEnded = { [weak self, weak animatingItemNode] in
|
||||
guard let strongSelf = self, let animatingItemNode = animatingItemNode else {
|
||||
return
|
||||
}
|
||||
animatingItemNode.removeFromSupernode()
|
||||
if let index = strongSelf.animatingItemNodes.firstIndex(where: { $0 === animatingItemNode }) {
|
||||
strongSelf.animatingItemNodes.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
animatingItemNode.frame = self.bounds
|
||||
animatingItemNode.beginAnimation()
|
||||
} else if let itemNode = itemNode as? ChatMessageStickerItemNode {
|
||||
|
||||
} else if let itemNode = itemNode as? ChatMessageAnimatedStickerItemNode {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) {
|
||||
for animatingItemNode in self.animatingItemNodes {
|
||||
animatingItemNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user