mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
338 lines
13 KiB
Swift
338 lines
13 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
import AccountContext
|
|
import ReactionSelectionNode
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
|
|
private let itemSize = CGSize(width: 110.0, height: 110.0)
|
|
|
|
final class ReactionCarouselNode: ASDisplayNode, UIScrollViewDelegate {
|
|
private let context: AccountContext
|
|
private let theme: PresentationTheme
|
|
private let reactions: [AvailableReactions.Reaction]
|
|
private var itemContainerNodes: [ASDisplayNode] = []
|
|
private var itemNodes: [ReactionNode] = []
|
|
private let scrollNode: ASScrollNode
|
|
private let tapNode: ASDisplayNode
|
|
|
|
private var standaloneReactionAnimation: StandaloneReactionAnimation?
|
|
private var animator: DisplayLinkAnimator?
|
|
private var currentPosition: CGFloat = 0.0
|
|
private var currentIndex: Int = 0
|
|
|
|
private var validLayout: CGSize?
|
|
|
|
private var playingIndices = Set<Int>()
|
|
|
|
private let positionDelta: Double
|
|
|
|
init(context: AccountContext, theme: PresentationTheme, reactions: [AvailableReactions.Reaction]) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.reactions = Array(reactions.shuffled().prefix(6))
|
|
|
|
self.scrollNode = ASScrollNode()
|
|
self.tapNode = ASDisplayNode()
|
|
|
|
self.positionDelta = 1.0 / CGFloat(self.reactions.count)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.scrollNode)
|
|
self.scrollNode.addSubnode(self.tapNode)
|
|
|
|
self.setup()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.scrollNode.view.delegate = self
|
|
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
|
self.scrollNode.view.canCancelContentTouches = true
|
|
|
|
self.tapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.reactionTapped(_:))))
|
|
}
|
|
|
|
@objc private func reactionTapped(_ gestureRecognizer: UITapGestureRecognizer) {
|
|
guard self.animator == nil, self.scrollStartPosition == nil else {
|
|
return
|
|
}
|
|
|
|
let point = gestureRecognizer.location(in: self.view)
|
|
guard let index = self.itemContainerNodes.firstIndex(where: { $0.frame.contains(point) }) else {
|
|
return
|
|
}
|
|
|
|
self.scrollTo(index, playReaction: true, duration: 0.4)
|
|
}
|
|
|
|
func animateIn() {
|
|
self.scrollTo(1, playReaction: true, duration: 0.5, clockwise: true)
|
|
}
|
|
|
|
func animateOut() {
|
|
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
|
standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
}
|
|
|
|
func scrollTo(_ index: Int, playReaction: Bool, duration: Double, clockwise: Bool? = nil) {
|
|
guard index >= 0 && index < self.itemNodes.count else {
|
|
return
|
|
}
|
|
self.currentIndex = index
|
|
let delta = self.positionDelta
|
|
|
|
let startPosition = self.currentPosition
|
|
let newPosition = delta * CGFloat(index)
|
|
var change = newPosition - startPosition
|
|
if let clockwise = clockwise {
|
|
if clockwise {
|
|
if change > 0.0 {
|
|
change = change - 1.0
|
|
}
|
|
} else {
|
|
if change < 0.0 {
|
|
change = 1.0 + change
|
|
}
|
|
}
|
|
} else {
|
|
if change > 0.5 {
|
|
change = change - 1.0
|
|
} else if change < -0.5 {
|
|
change = 1.0 + change
|
|
}
|
|
}
|
|
|
|
self.animator = DisplayLinkAnimator(duration: duration * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] t in
|
|
let t = listViewAnimationCurveSystem(t)
|
|
var updatedPosition = startPosition + change * t
|
|
while updatedPosition >= 1.0 {
|
|
updatedPosition -= 1.0
|
|
}
|
|
while updatedPosition < 0.0 {
|
|
updatedPosition += 1.0
|
|
}
|
|
self?.currentPosition = updatedPosition
|
|
if let size = self?.validLayout {
|
|
self?.updateLayout(size: size, transition: .immediate)
|
|
}
|
|
}, completion: { [weak self] in
|
|
self?.animator = nil
|
|
if playReaction {
|
|
self?.playReaction()
|
|
}
|
|
})
|
|
}
|
|
|
|
func setup() {
|
|
for reaction in self.reactions {
|
|
guard let centerAnimation = reaction.centerAnimation else {
|
|
continue
|
|
}
|
|
guard let aroundAnimation = reaction.aroundAnimation else {
|
|
continue
|
|
}
|
|
let containerNode = ASDisplayNode()
|
|
let itemNode = ReactionNode(context: self.context, theme: self.theme, item: ReactionItem(
|
|
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
|
appearAnimation: reaction.appearAnimation,
|
|
stillAnimation: reaction.selectAnimation,
|
|
listAnimation: centerAnimation,
|
|
largeListAnimation: reaction.activateAnimation,
|
|
applicationAnimation: aroundAnimation,
|
|
largeApplicationAnimation: reaction.effectAnimation
|
|
), hasAppearAnimation: false)
|
|
containerNode.isUserInteractionEnabled = false
|
|
containerNode.addSubnode(itemNode)
|
|
self.addSubnode(containerNode)
|
|
|
|
self.itemContainerNodes.append(containerNode)
|
|
self.itemNodes.append(itemNode)
|
|
}
|
|
}
|
|
|
|
private var ignoreContentOffsetChange = false
|
|
private func resetScrollPosition() {
|
|
self.scrollStartPosition = nil
|
|
self.ignoreContentOffsetChange = true
|
|
self.scrollNode.view.contentOffset = CGPoint(x: 5000.0 - self.scrollNode.frame.width * 0.5, y: 0.0)
|
|
self.ignoreContentOffsetChange = false
|
|
}
|
|
|
|
func playReaction() {
|
|
let delta = self.positionDelta
|
|
let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta))))
|
|
|
|
guard !self.playingIndices.contains(index) else {
|
|
return
|
|
}
|
|
|
|
let reaction = self.reactions[index]
|
|
let targetContainerNode = self.itemContainerNodes[index]
|
|
let targetView = self.itemNodes[index].view
|
|
|
|
guard let centerAnimation = reaction.centerAnimation else {
|
|
return
|
|
}
|
|
guard let aroundAnimation = reaction.aroundAnimation else {
|
|
return
|
|
}
|
|
|
|
self.playingIndices.insert(index)
|
|
|
|
targetContainerNode.view.superview?.bringSubviewToFront(targetContainerNode.view)
|
|
|
|
let standaloneReactionAnimation = StandaloneReactionAnimation()
|
|
self.standaloneReactionAnimation = standaloneReactionAnimation
|
|
|
|
targetContainerNode.addSubnode(standaloneReactionAnimation)
|
|
standaloneReactionAnimation.frame = targetContainerNode.bounds
|
|
standaloneReactionAnimation.animateReactionSelection(
|
|
context: self.context, theme: self.theme, reaction: ReactionItem(
|
|
reaction: ReactionItem.Reaction(rawValue: reaction.value),
|
|
appearAnimation: reaction.appearAnimation,
|
|
stillAnimation: reaction.selectAnimation,
|
|
listAnimation: centerAnimation,
|
|
largeListAnimation: reaction.activateAnimation,
|
|
applicationAnimation: aroundAnimation,
|
|
largeApplicationAnimation: reaction.effectAnimation
|
|
),
|
|
avatarPeers: [],
|
|
playHaptic: false,
|
|
isLarge: true,
|
|
forceSmallEffectAnimation: true,
|
|
targetView: targetView,
|
|
addStandaloneReactionAnimation: nil,
|
|
completion: { [weak standaloneReactionAnimation, weak self] in
|
|
standaloneReactionAnimation?.removeFromSupernode()
|
|
self?.standaloneReactionAnimation = nil
|
|
self?.playingIndices.remove(index)
|
|
}
|
|
)
|
|
}
|
|
|
|
private var scrollStartPosition: (contentOffset: CGFloat, position: CGFloat)?
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
if self.scrollStartPosition == nil {
|
|
self.scrollStartPosition = (scrollView.contentOffset.x, self.currentPosition)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
guard !self.ignoreContentOffsetChange, let (startContentOffset, startPosition) = self.scrollStartPosition else {
|
|
return
|
|
}
|
|
|
|
let delta = scrollView.contentOffset.x - startContentOffset
|
|
let positionDelta = delta * -0.001
|
|
var updatedPosition = startPosition + positionDelta
|
|
while updatedPosition >= 1.0 {
|
|
updatedPosition -= 1.0
|
|
}
|
|
while updatedPosition < 0.0 {
|
|
updatedPosition += 1.0
|
|
}
|
|
self.currentPosition = updatedPosition
|
|
|
|
let indexDelta = self.positionDelta
|
|
let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / indexDelta))))
|
|
if index != self.currentIndex {
|
|
self.currentIndex = index
|
|
print(index)
|
|
}
|
|
|
|
if let size = self.validLayout {
|
|
self.updateLayout(size: size, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
|
guard let (startContentOffset, _) = self.scrollStartPosition, abs(velocity.x) > 0.0 else {
|
|
return
|
|
}
|
|
|
|
let delta = self.positionDelta
|
|
let scrollDelta = targetContentOffset.pointee.x - startContentOffset
|
|
let positionDelta = scrollDelta * -0.001
|
|
let positionCounts = round(positionDelta / delta)
|
|
let adjustedPositionDelta = delta * positionCounts
|
|
let adjustedScrollDelta = adjustedPositionDelta * -1000.0
|
|
|
|
targetContentOffset.pointee = CGPoint(x: startContentOffset + adjustedScrollDelta, y: 0.0)
|
|
}
|
|
|
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
if !decelerate {
|
|
self.resetScrollPosition()
|
|
|
|
let delta = self.positionDelta
|
|
let index = max(0, min(self.itemNodes.count - 1, Int(round(self.currentPosition / delta))))
|
|
self.scrollTo(index, playReaction: true, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
self.resetScrollPosition()
|
|
self.playReaction()
|
|
}
|
|
|
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = size
|
|
|
|
self.scrollNode.frame = CGRect(origin: CGPoint(), size: size)
|
|
if self.scrollNode.view.contentSize.width.isZero {
|
|
self.scrollNode.view.contentSize = CGSize(width: 10000000, height: size.height)
|
|
self.tapNode.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)
|
|
self.resetScrollPosition()
|
|
}
|
|
|
|
let delta = self.positionDelta
|
|
|
|
let areaSize = CGSize(width: floor(size.width * 0.7), height: size.height * 0.5)
|
|
|
|
for i in 0 ..< self.itemNodes.count {
|
|
let itemNode = self.itemNodes[i]
|
|
let containerNode = self.itemContainerNodes[i]
|
|
|
|
var angle = CGFloat.pi * 0.5 + CGFloat(i) * delta * CGFloat.pi * 2.0 - self.currentPosition * CGFloat.pi * 2.0
|
|
if angle < 0.0 {
|
|
angle = CGFloat.pi * 2.0 + angle
|
|
}
|
|
if angle > CGFloat.pi * 2.0 {
|
|
angle = angle - CGFloat.pi * 2.0
|
|
}
|
|
|
|
func calculateRelativeAngle(_ angle: CGFloat) -> CGFloat {
|
|
var relativeAngle = angle - CGFloat.pi * 0.5
|
|
if relativeAngle > CGFloat.pi {
|
|
relativeAngle = (2.0 * CGFloat.pi - relativeAngle) * -1.0
|
|
}
|
|
return relativeAngle
|
|
}
|
|
|
|
let relativeAngle = calculateRelativeAngle(angle)
|
|
let distance = abs(relativeAngle) / CGFloat.pi
|
|
|
|
let point = CGPoint(
|
|
x: cos(angle),
|
|
y: sin(angle)
|
|
)
|
|
|
|
let itemFrame = CGRect(origin: CGPoint(x: size.width * 0.5 + point.x * areaSize.width * 0.5 - itemSize.width * 0.5, y: size.height * 0.5 + point.y * areaSize.height * 0.5 - itemSize.height * 0.5), size: itemSize)
|
|
containerNode.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
|
|
containerNode.position = itemFrame.center
|
|
transition.updateTransformScale(node: containerNode, scale: 1.0 - distance * 0.45)
|
|
|
|
itemNode.frame = CGRect(origin: CGPoint(), size: itemFrame.size)
|
|
itemNode.updateLayout(size: itemFrame.size, isExpanded: false, largeExpanded: false, isPreviewing: false, transition: transition)
|
|
|
|
}
|
|
}
|
|
}
|