mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
451 lines
20 KiB
Swift
451 lines
20 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import TelegramCore
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import TelegramUIPreferences
|
|
import ContextUI
|
|
|
|
private final class InstantPageSlideshowItemNode: ASDisplayNode {
|
|
private var _index: Int?
|
|
var index: Int {
|
|
get {
|
|
return self._index!
|
|
} set(value) {
|
|
self._index = value
|
|
}
|
|
}
|
|
private let contentNode: ASDisplayNode
|
|
|
|
var internalIsVisible: Bool = false {
|
|
didSet {
|
|
if self.internalParentVisible && oldValue != self.internalIsVisible && self.internalParentVisible {
|
|
(self.contentNode as? InstantPageNode)?.updateIsVisible(self.internalIsVisible && self.internalParentVisible)
|
|
}
|
|
}
|
|
}
|
|
|
|
var internalParentVisible: Bool = false {
|
|
didSet {
|
|
if self.internalIsVisible && oldValue != self.internalIsVisible && self.internalParentVisible {
|
|
(self.contentNode as? InstantPageNode)?.updateIsVisible(self.internalIsVisible && self.internalParentVisible)
|
|
}
|
|
}
|
|
}
|
|
|
|
init(contentNode: ASDisplayNode) {
|
|
self.contentNode = contentNode
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.contentNode)
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
|
|
self.contentNode.frame = self.bounds
|
|
}
|
|
|
|
func updateHiddenMedia(_ media: InstantPageMedia?) {
|
|
if let node = self.contentNode as? InstantPageNode {
|
|
node.updateHiddenMedia(media: media)
|
|
}
|
|
}
|
|
|
|
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
if let node = self.contentNode as? InstantPageNode {
|
|
return node.transitionNode(media: media)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDelegate {
|
|
private let context: AccountContext
|
|
private let sourceLocation: InstantPageSourceLocation
|
|
private let theme: InstantPageTheme
|
|
private let webPage: TelegramMediaWebpage
|
|
private let openMedia: (InstantPageMedia) -> Void
|
|
private let longPressMedia: (InstantPageMedia) -> Void
|
|
private let activatePinchPreview: ((PinchSourceContainerNode) -> Void)?
|
|
private let pinchPreviewFinished: ((InstantPageNode) -> Void)?
|
|
private let pageGap: CGFloat
|
|
|
|
private let scrollView: UIScrollView
|
|
|
|
private var items: [InstantPageMedia] = []
|
|
private var itemNodes: [InstantPageSlideshowItemNode] = []
|
|
private var ignoreCentralItemIndexUpdate = false
|
|
private var centralItemIndex: Int? {
|
|
didSet {
|
|
if oldValue != self.centralItemIndex && !self.ignoreCentralItemIndexUpdate {
|
|
//self.centralItemIndexUpdated(self.centralItemIndex)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var containerLayout: ContainerViewLayout?
|
|
|
|
var centralItemIndexUpdated: (Int?) -> Void = { _ in }
|
|
|
|
var internalIsVisible: Bool = false {
|
|
didSet {
|
|
if self.internalIsVisible != oldValue {
|
|
for node in self.itemNodes {
|
|
node.internalParentVisible = self.internalIsVisible
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?, pageGap: CGFloat = 0.0) {
|
|
self.context = context
|
|
self.sourceLocation = sourceLocation
|
|
self.theme = theme
|
|
self.webPage = webPage
|
|
self.openMedia = openMedia
|
|
self.longPressMedia = longPressMedia
|
|
self.activatePinchPreview = activatePinchPreview
|
|
self.pinchPreviewFinished = pinchPreviewFinished
|
|
self.pageGap = pageGap
|
|
self.scrollView = UIScrollView()
|
|
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
|
self.scrollView.contentInsetAdjustmentBehavior = .never
|
|
}
|
|
|
|
super.init()
|
|
|
|
self.scrollView.showsVerticalScrollIndicator = false
|
|
self.scrollView.showsHorizontalScrollIndicator = false
|
|
self.scrollView.alwaysBounceHorizontal = !pageGap.isZero
|
|
self.scrollView.bounces = !pageGap.isZero
|
|
self.scrollView.isPagingEnabled = true
|
|
self.scrollView.delegate = self
|
|
self.scrollView.clipsToBounds = false
|
|
self.scrollView.scrollsToTop = false
|
|
self.view.addSubview(self.scrollView)
|
|
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
self.containerLayout = layout
|
|
|
|
var previousCentralNodeHorizontalOffset: CGFloat?
|
|
if let centralItemIndex = self.centralItemIndex, let centralNode = self.visibleItemNode(at: centralItemIndex) {
|
|
previousCentralNodeHorizontalOffset = self.scrollView.contentOffset.x - centralNode.frame.minX
|
|
}
|
|
|
|
self.scrollView.frame = CGRect(origin: CGPoint(x: -self.pageGap, y: 0.0), size: CGSize(width: layout.size.width + self.pageGap * 2.0, height: layout.size.height))
|
|
|
|
for i in 0 ..< self.itemNodes.count {
|
|
self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height))
|
|
//self.itemNodes[i].containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
|
}
|
|
|
|
if let previousCentralNodeHorizontalOffset = previousCentralNodeHorizontalOffset, let centralItemIndex = self.centralItemIndex, let centralNode = self.visibleItemNode(at: centralItemIndex) {
|
|
self.scrollView.contentOffset = CGPoint(x: centralNode.frame.minX + previousCentralNodeHorizontalOffset, y: 0.0)
|
|
}
|
|
|
|
self.updateItemNodes()
|
|
}
|
|
|
|
func centralItemNode() -> InstantPageSlideshowItemNode? {
|
|
if let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) {
|
|
return centralItemNode
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func replaceItems(_ items: [InstantPageMedia], centralItemIndex: Int?, keepFirst: Bool = false) {
|
|
var keptItemNode: InstantPageSlideshowItemNode?
|
|
for itemNode in self.itemNodes {
|
|
if keepFirst && itemNode.index == 0 {
|
|
keptItemNode = itemNode
|
|
} else {
|
|
itemNode.removeFromSupernode()
|
|
}
|
|
}
|
|
self.itemNodes.removeAll()
|
|
if let keptItemNode = keptItemNode {
|
|
self.itemNodes.append(keptItemNode)
|
|
}
|
|
if let centralItemIndex = centralItemIndex, centralItemIndex >= 0 && centralItemIndex < items.count {
|
|
self.centralItemIndex = centralItemIndex
|
|
} else {
|
|
self.centralItemIndex = nil
|
|
}
|
|
self.items = items
|
|
|
|
self.updateItemNodes()
|
|
}
|
|
|
|
private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode {
|
|
let media = self.items[index]
|
|
let contentNode: ASDisplayNode
|
|
if case .image = media.media {
|
|
contentNode = InstantPageImageNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: self.activatePinchPreview, pinchPreviewFinished: self.pinchPreviewFinished)
|
|
} else if case .file = media.media {
|
|
contentNode = ASDisplayNode()
|
|
} else {
|
|
contentNode = ASDisplayNode()
|
|
}
|
|
|
|
let node = InstantPageSlideshowItemNode(contentNode: contentNode)
|
|
|
|
node.index = index
|
|
return node
|
|
}
|
|
|
|
private func visibleItemNode(at index: Int) -> InstantPageSlideshowItemNode? {
|
|
for itemNode in self.itemNodes {
|
|
if itemNode.index == index {
|
|
return itemNode
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private func addVisibleItemNode(_ node: InstantPageSlideshowItemNode) {
|
|
var added = false
|
|
for i in 0 ..< self.itemNodes.count {
|
|
if node.index < self.itemNodes[i].index {
|
|
self.itemNodes.insert(node, at: i)
|
|
added = true
|
|
break
|
|
}
|
|
}
|
|
if !added {
|
|
self.itemNodes.append(node)
|
|
}
|
|
self.scrollView.addSubview(node.view)
|
|
}
|
|
|
|
private func removeVisibleItemNode(internalIndex: Int) {
|
|
self.itemNodes[internalIndex].view.removeFromSuperview()
|
|
self.itemNodes.remove(at: internalIndex)
|
|
}
|
|
|
|
private func updateItemNodes() {
|
|
if self.items.isEmpty || self.containerLayout == nil {
|
|
return
|
|
}
|
|
|
|
var resetOffsetToCentralItem = false
|
|
if self.itemNodes.isEmpty {
|
|
let node = self.makeNodeForItem(at: self.centralItemIndex ?? 0)
|
|
node.frame = CGRect(origin: CGPoint(), size: scrollView.bounds.size)
|
|
if let _ = self.containerLayout {
|
|
//node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate)
|
|
}
|
|
self.addVisibleItemNode(node)
|
|
self.centralItemIndex = node.index
|
|
resetOffsetToCentralItem = true
|
|
}
|
|
|
|
var notifyCentralItemUpdated = false
|
|
|
|
if let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) {
|
|
if centralItemIndex != 0 {
|
|
if self.visibleItemNode(at: centralItemIndex - 1) == nil {
|
|
let node = self.makeNodeForItem(at: centralItemIndex - 1)
|
|
node.frame = centralItemNode.frame.offsetBy(dx: -centralItemNode.frame.size.width - self.pageGap, dy: 0.0)
|
|
if let _ = self.containerLayout {
|
|
//node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate)
|
|
}
|
|
self.addVisibleItemNode(node)
|
|
}
|
|
}
|
|
|
|
if centralItemIndex != items.count - 1 {
|
|
if self.visibleItemNode(at: centralItemIndex + 1) == nil {
|
|
let node = self.makeNodeForItem(at: centralItemIndex + 1)
|
|
node.frame = centralItemNode.frame.offsetBy(dx: centralItemNode.frame.size.width + self.pageGap, dy: 0.0)
|
|
if let _ = self.containerLayout {
|
|
//node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate)
|
|
}
|
|
self.addVisibleItemNode(node)
|
|
}
|
|
}
|
|
|
|
for i in 0 ..< self.itemNodes.count {
|
|
self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height))
|
|
}
|
|
|
|
if resetOffsetToCentralItem {
|
|
self.scrollView.contentOffset = CGPoint(x: centralItemNode.frame.minX - self.pageGap, y: 0.0)
|
|
}
|
|
|
|
if let centralItemCandidateNode = self.centralItemCandidate(), centralItemCandidateNode.index != centralItemIndex {
|
|
for i in (0 ..< self.itemNodes.count).reversed() {
|
|
let node = self.itemNodes[i]
|
|
if node.index < centralItemCandidateNode.index - 1 || node.index > centralItemCandidateNode.index + 1 {
|
|
self.removeVisibleItemNode(internalIndex: i)
|
|
}
|
|
}
|
|
|
|
self.ignoreCentralItemIndexUpdate = true
|
|
self.centralItemIndex = centralItemCandidateNode.index
|
|
self.ignoreCentralItemIndexUpdate = false
|
|
notifyCentralItemUpdated = true
|
|
|
|
if centralItemCandidateNode.index != 0 {
|
|
if self.visibleItemNode(at: centralItemCandidateNode.index - 1) == nil {
|
|
let node = self.makeNodeForItem(at: centralItemCandidateNode.index - 1)
|
|
node.frame = centralItemCandidateNode.frame.offsetBy(dx: -centralItemCandidateNode.frame.size.width - self.pageGap, dy: 0.0)
|
|
if let _ = self.containerLayout {
|
|
//node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate)
|
|
}
|
|
self.addVisibleItemNode(node)
|
|
}
|
|
}
|
|
|
|
if centralItemCandidateNode.index != items.count - 1 {
|
|
if self.visibleItemNode(at: centralItemCandidateNode.index + 1) == nil {
|
|
let node = self.makeNodeForItem(at: centralItemCandidateNode.index + 1)
|
|
node.frame = centralItemCandidateNode.frame.offsetBy(dx: centralItemCandidateNode.frame.size.width + self.pageGap, dy: 0.0)
|
|
if let _ = self.containerLayout {
|
|
//node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate)
|
|
}
|
|
self.addVisibleItemNode(node)
|
|
}
|
|
}
|
|
|
|
let previousCentralCandidateHorizontalOffset = self.scrollView.contentOffset.x - centralItemCandidateNode.frame.minX
|
|
|
|
for i in 0 ..< self.itemNodes.count {
|
|
self.itemNodes[i].frame = CGRect(origin: CGPoint(x: CGFloat(i) * self.scrollView.bounds.size.width + self.pageGap, y: 0.0), size: CGSize(width: self.scrollView.bounds.size.width - self.pageGap * 2.0, height: self.scrollView.bounds.size.height))
|
|
}
|
|
|
|
self.scrollView.contentOffset = CGPoint(x: centralItemCandidateNode.frame.minX + previousCentralCandidateHorizontalOffset, y: 0.0)
|
|
}
|
|
|
|
self.scrollView.contentSize = CGSize(width: CGFloat(self.itemNodes.count) * self.scrollView.bounds.size.width, height: self.scrollView.bounds.size.height)
|
|
} else {
|
|
assertionFailure()
|
|
}
|
|
|
|
for itemNode in self.itemNodes {
|
|
//itemNode.centralityUpdated(isCentral: itemNode.index == self.centralItemIndex)
|
|
//itemNode.visibilityUpdated(isVisible: self.scrollView.bounds.intersects(itemNode.frame))
|
|
itemNode.internalIsVisible = self.scrollView.bounds.intersects(itemNode.frame)
|
|
}
|
|
|
|
if notifyCentralItemUpdated {
|
|
self.centralItemIndexUpdated(self.centralItemIndex)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
self.updateItemNodes()
|
|
}
|
|
|
|
private func centralItemCandidate() -> InstantPageSlideshowItemNode? {
|
|
let hotizontlOffset = self.scrollView.contentOffset.x + self.pageGap
|
|
var closestNodeAndDistance: (Int, CGFloat)?
|
|
for i in 0 ..< self.itemNodes.count {
|
|
let node = self.itemNodes[i]
|
|
let distance = abs(node.frame.minX - hotizontlOffset)
|
|
if let currentClosestNodeAndDistance = closestNodeAndDistance {
|
|
if distance < currentClosestNodeAndDistance.1 {
|
|
closestNodeAndDistance = (node.index, distance)
|
|
}
|
|
} else {
|
|
closestNodeAndDistance = (node.index, distance)
|
|
}
|
|
}
|
|
if let closestNodeAndDistance = closestNodeAndDistance {
|
|
return self.visibleItemNode(at: closestNodeAndDistance.0)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func updateHiddenMedia(_ media: InstantPageMedia?) {
|
|
for node in self.itemNodes {
|
|
node.updateHiddenMedia(media)
|
|
}
|
|
}
|
|
|
|
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
for node in self.itemNodes {
|
|
if let transitionNode = node.transitionNode(media: media) {
|
|
return transitionNode
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
final class InstantPageSlideshowNode: ASDisplayNode, InstantPageNode {
|
|
var medias: [InstantPageMedia] = []
|
|
|
|
private let pagerNode: InstantPageSlideshowPagerNode
|
|
private let pageControlNode: PageControlNode
|
|
|
|
init(context: AccountContext, sourceLocation: InstantPageSourceLocation, theme: InstantPageTheme, webPage: TelegramMediaWebpage, medias: [InstantPageMedia], openMedia: @escaping (InstantPageMedia) -> Void, longPressMedia: @escaping (InstantPageMedia) -> Void, activatePinchPreview: ((PinchSourceContainerNode) -> Void)?, pinchPreviewFinished: ((InstantPageNode) -> Void)?) {
|
|
self.medias = medias
|
|
|
|
self.pagerNode = InstantPageSlideshowPagerNode(context: context, sourceLocation: sourceLocation, theme: theme, webPage: webPage, openMedia: openMedia, longPressMedia: longPressMedia, activatePinchPreview: activatePinchPreview, pinchPreviewFinished: pinchPreviewFinished)
|
|
self.pagerNode.replaceItems(medias, centralItemIndex: nil)
|
|
|
|
self.pageControlNode = PageControlNode(dotColor: .white, inactiveDotColor: UIColor(white: 1.0, alpha: 0.5))
|
|
self.pageControlNode.isUserInteractionEnabled = false
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = theme.panelSecondaryColor
|
|
self.clipsToBounds = true
|
|
|
|
self.addSubnode(self.pagerNode)
|
|
self.addSubnode(self.pageControlNode)
|
|
self.pageControlNode.pagesCount = medias.count
|
|
self.pageControlNode.setPage(0)
|
|
self.pagerNode.centralItemIndexUpdated = { [weak self] index in
|
|
if let strongSelf = self, let index = index {
|
|
strongSelf.pageControlNode.setPage(CGFloat(index))
|
|
}
|
|
}
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
|
|
self.pagerNode.frame = self.bounds
|
|
self.pagerNode.containerLayoutUpdated(ContainerViewLayout(size: self.bounds.size, metrics: LayoutMetrics(), deviceMetrics: .unknown(screenSize: CGSize(), statusBarHeight: 0.0, onScreenNavigationHeight: nil, screenCornerRadius: 0.0), intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), transition: .immediate)
|
|
|
|
self.pageControlNode.layer.transform = CATransform3DIdentity
|
|
self.pageControlNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height - 20.0), size: CGSize(width: self.bounds.size.width, height: 20.0))
|
|
|
|
let maxWidth = self.bounds.width - 36.0;
|
|
let size = self.pageControlNode.calculateSizeThatFits(self.bounds.size)
|
|
if size.width > maxWidth
|
|
{
|
|
let scale = maxWidth / size.width
|
|
self.pageControlNode.layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
|
}
|
|
}
|
|
|
|
func transitionNode(media: InstantPageMedia) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
|
|
return self.pagerNode.transitionNode(media: media)
|
|
}
|
|
|
|
func updateHiddenMedia(media: InstantPageMedia?) {
|
|
self.pagerNode.updateHiddenMedia(media)
|
|
}
|
|
|
|
func updateIsVisible(_ isVisible: Bool) {
|
|
self.pagerNode.internalIsVisible = isVisible
|
|
}
|
|
|
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
}
|
|
|
|
func update(strings: PresentationStrings, theme: InstantPageTheme) {
|
|
self.backgroundColor = theme.panelSecondaryColor
|
|
}
|
|
}
|