import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox private func edgeWidth(width: CGFloat) -> CGFloat { return min(44.0, floor(width / 6.0)) } let fadeWidth: CGFloat = 70.0 private let leftFadeImage = generateImage(CGSize(width: fadeWidth, height: 32.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) let gradientColors = [UIColor.black.withAlphaComponent(0.35).cgColor, UIColor.black.withAlphaComponent(0.0).cgColor] as CFArray var locations: [CGFloat] = [0.0, 1.0] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) }) private let rightFadeImage = generateImage(CGSize(width: fadeWidth, height: 32.0), opaque: false, rotatedContext: { size, context in let bounds = CGRect(origin: CGPoint(), size: size) context.clear(bounds) let gradientColors = [UIColor.black.withAlphaComponent(0.0).cgColor, UIColor.black.withAlphaComponent(0.35).cgColor] as CFArray var locations: [CGFloat] = [0.0, 1.0] let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) }) public struct GalleryPagerInsertItem { public let index: Int public let item: GalleryItem public let previousIndex: Int? public init(index: Int, item: GalleryItem, previousIndex: Int?) { self.index = index self.item = item self.previousIndex = previousIndex } } public struct GalleryPagerUpdateItem { public let index: Int public let previousIndex: Int public let item: GalleryItem public init(index: Int, previousIndex: Int, item: GalleryItem) { self.index = index self.previousIndex = previousIndex self.item = item } } public struct GalleryPagerTransaction { public let deleteItems: [Int] public let insertItems: [GalleryPagerInsertItem] public let updateItems: [GalleryPagerUpdateItem] public let focusOnItem: Int? public let synchronous: Bool public init(deleteItems: [Int], insertItems: [GalleryPagerInsertItem], updateItems: [GalleryPagerUpdateItem], focusOnItem: Int?, synchronous: Bool) { self.deleteItems = deleteItems self.insertItems = insertItems self.updateItems = updateItems self.focusOnItem = focusOnItem self.synchronous = synchronous } } public final class GalleryPagerNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private let pageGap: CGFloat private let disableTapNavigation: Bool private let scrollView: UIScrollView private let leftFadeNode: ASDisplayNode private let rightFadeNode: ASDisplayNode private var highlightedSide: Bool? private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? public private(set) var items: [GalleryItem] = [] private var itemNodes: [GalleryItemNode] = [] private var ignoreDidScroll = false private var ignoreCentralItemIndexUpdate = false private var centralItemIndex: Int? { didSet { if oldValue != self.centralItemIndex && !self.ignoreCentralItemIndexUpdate { self.centralItemIndexUpdated(self.centralItemIndex) } } } private var containerLayout: (ContainerViewLayout, CGFloat)? public var centralItemIndexUpdated: (Int?) -> Void = { _ in } private var invalidatedItems = false public var centralItemIndexOffsetUpdated: (([GalleryItem]?, Int, CGFloat)?) -> Void = { _ in } public var toggleControlsVisibility: () -> Void = { } public var updateControlsVisibility: (Bool) -> Void = { _ in } public var updateOrientation: (UIInterfaceOrientation) -> Void = { _ in } public var dismiss: () -> Void = { } public var beginCustomDismiss: (Bool) -> Void = { _ in } public var completeCustomDismiss: () -> Void = { } public var baseNavigationController: () -> NavigationController? = { return nil } public var galleryController: () -> ViewController? = { return nil } private var pagingEnabled = true public var pagingEnabledPromise = Promise(true) private var pagingEnabledDisposable: Disposable? public init(pageGap: CGFloat, disableTapNavigation: Bool) { self.pageGap = pageGap self.disableTapNavigation = disableTapNavigation self.scrollView = UIScrollView() if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.scrollView.contentInsetAdjustmentBehavior = .never } self.leftFadeNode = ASDisplayNode() self.leftFadeNode.alpha = 0.0 self.leftFadeNode.backgroundColor = leftFadeImage.flatMap { UIColor(patternImage: $0) } self.rightFadeNode = ASDisplayNode() self.rightFadeNode.alpha = 0.0 self.rightFadeNode.backgroundColor = rightFadeImage.flatMap { UIColor(patternImage: $0) } 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.scrollView.delaysContentTouches = false self.view.addSubview(self.scrollView) self.addSubnode(self.leftFadeNode) self.addSubnode(self.rightFadeNode) self.pagingEnabledDisposable = (self.pagingEnabledPromise.get() |> deliverOnMainQueue).start(next: { [weak self] pagingEnabled in if let strongSelf = self { strongSelf.pagingEnabled = pagingEnabled } }).strict() } deinit { self.pagingEnabledDisposable?.dispose() } public override func didLoad() { super.didLoad() let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) recognizer.delegate = self self.tapRecognizer = recognizer recognizer.tapActionAtPoint = { [weak self] point in guard let strongSelf = self, strongSelf.pagingEnabled else { return .fail } let size = strongSelf.bounds var highlightedSide: Bool? if point.x < edgeWidth(width: size.width) && strongSelf.canGoToPreviousItem() { if strongSelf.items.count > 1 { highlightedSide = false } } else if point.x > size.width - edgeWidth(width: size.width) && strongSelf.canGoToNextItem() { if strongSelf.items.count > 1 { if point.y < 80.0 { highlightedSide = nil } else { highlightedSide = true } } } if highlightedSide == nil { return .fail } if let result = strongSelf.hitTest(point, with: nil), let _ = result.asyncdisplaykit_node as? ASButtonNode { return .fail } return .keepWithSingleTap } recognizer.highlight = { [weak self] point in guard let strongSelf = self, strongSelf.pagingEnabled else { return } let size = strongSelf.bounds var highlightedSide: Bool? if let point = point { if point.x < edgeWidth(width: size.width) && strongSelf.canGoToPreviousItem() { if strongSelf.items.count > 1 { highlightedSide = false } } else if point.x > size.width - edgeWidth(width: size.width) && strongSelf.canGoToNextItem() { if strongSelf.items.count > 1 { highlightedSide = true } } } if strongSelf.highlightedSide != highlightedSide { strongSelf.highlightedSide = highlightedSide let leftAlpha: CGFloat let rightAlpha: CGFloat if let highlightedSide = highlightedSide { leftAlpha = highlightedSide ? 0.0 : 1.0 rightAlpha = highlightedSide ? 1.0 : 0.0 } else { leftAlpha = 0.0 rightAlpha = 0.0 } if strongSelf.leftFadeNode.alpha != leftAlpha { strongSelf.leftFadeNode.alpha = leftAlpha if leftAlpha.isZero { strongSelf.leftFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) } else { strongSelf.leftFadeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08) } } if strongSelf.rightFadeNode.alpha != rightAlpha { strongSelf.rightFadeNode.alpha = rightAlpha if rightAlpha.isZero { strongSelf.rightFadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.16, timingFunction: kCAMediaTimingFunctionSpring) } else { strongSelf.rightFadeNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.08) } } } } self.view.addGestureRecognizer(recognizer) } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @objc private func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { if case .tap = gesture { let size = self.bounds.size if location.x < edgeWidth(width: size.width) && self.canGoToPreviousItem() { self.goToPreviousItem() } else if location.x > size.width - edgeWidth(width: size.width) && self.canGoToNextItem() { self.goToNextItem() } } } default: break } } public var isScrollEnabled: Bool { get { return self.scrollView.isScrollEnabled } set { self.scrollView.isScrollEnabled = newValue } } public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight) var centralPoint: CGPoint? if transition.isAnimated, let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { centralPoint = self.view.convert(CGPoint(x: centralItemNode.frame.size.width / 2.0, y: centralItemNode.frame.size.height / 2.0), from: centralItemNode.view) } var previousCentralNodeHorizontalOffset: CGFloat? if let centralItemIndex = self.centralItemIndex, let centralNode = self.visibleItemNode(at: centralItemIndex) { previousCentralNodeHorizontalOffset = self.scrollView.contentOffset.x - centralNode.frame.minX } self.ignoreDidScroll = true 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)) self.ignoreDidScroll = false for i in 0 ..< self.itemNodes.count { transition.updateFrame(node: 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(transition: transition) if let centralPoint = centralPoint, let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { let updatedCentralPoint = self.view.convert(CGPoint(x: centralItemNode.frame.size.width / 2.0, y: centralItemNode.frame.size.height / 2.0), from: centralItemNode.view) transition.animatePosition(node: centralItemNode, from: centralItemNode.position.offsetBy(dx: -updatedCentralPoint.x + centralPoint.x, dy: -updatedCentralPoint.y + centralPoint.y)) } self.leftFadeNode.frame = CGRect(x: 0.0, y: 0.0, width: fadeWidth, height: layout.size.height) self.rightFadeNode.frame = CGRect(x: layout.size.width - fadeWidth, y: 0.0, width: fadeWidth, height: layout.size.height) } public func ready() -> Signal { if let itemNode = self.centralItemNode() { return itemNode.ready() } return .single(Void()) } public func centralItemNode() -> GalleryItemNode? { if let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { return centralItemNode } else { return nil } } public var updateOnReplacement = false public func replaceItems(_ items: [GalleryItem], centralItemIndex: Int?, synchronous: Bool = false) { var updateItems: [GalleryPagerUpdateItem] = [] var deleteItems: [Int] = [] var insertItems: [GalleryPagerInsertItem] = [] var previousIndexById: [AnyHashable: Int] = [:] let validIds = Set(items.map { $0.id }) for i in 0 ..< self.items.count { previousIndexById[self.items[i].id] = i if !validIds.contains(self.items[i].id) { deleteItems.append(i) } } if self.updateOnReplacement { for i in 0 ..< items.count { if (previousIndexById[items[i].id] == nil) { insertItems.append(GalleryPagerInsertItem(index: i, item: items[i], previousIndex: previousIndexById[items[i].id])) } else { updateItems.append(GalleryPagerUpdateItem(index: i, previousIndex: i, item: items[i])) } } } else { for i in 0 ..< items.count { insertItems.append(GalleryPagerInsertItem(index: i, item: items[i], previousIndex: previousIndexById[items[i].id])) } } self.transaction(GalleryPagerTransaction(deleteItems: deleteItems, insertItems: insertItems, updateItems: updateItems, focusOnItem: centralItemIndex, synchronous: synchronous)) if self.updateOnReplacement { self.items = items for i in 0 ..< self.items.count { if let itemNode = self.visibleItemNode(at: i) { self.items[i].updateNode(node: itemNode, synchronous: synchronous) } } for i in (0 ..< self.itemNodes.count).reversed() { let node = self.itemNodes[i] if node.index > self.items.count - 1 { node.removeFromSupernode() self.itemNodes.remove(at: i) } } self.updateCentralIndexOffset(transition: .immediate) } } public func transaction(_ transaction: GalleryPagerTransaction) { for updatedItem in transaction.updateItems { self.items[updatedItem.previousIndex] = updatedItem.item if let itemNode = self.visibleItemNode(at: updatedItem.previousIndex) { //print("update visible node at \(updatedItem.previousIndex)") updatedItem.item.updateNode(node: itemNode, synchronous: transaction.synchronous) } } if !transaction.deleteItems.isEmpty || !transaction.insertItems.isEmpty { let deleteItems = transaction.deleteItems.sorted() for deleteItemIndex in deleteItems.reversed() { self.items.remove(at: deleteItemIndex) for i in 0 ..< self.itemNodes.count { if self.itemNodes[i].index == deleteItemIndex { //print("delete visible node at \(deleteItemIndex)") self.removeVisibleItemNode(internalIndex: i) break } } } let insertItems = transaction.insertItems.sorted(by: { $0.index < $1.index }) if transaction.updateItems.isEmpty && !insertItems.isEmpty { self.items.removeAll() } for insertedItem in insertItems { self.items.append(insertedItem.item) //self.items.insert(insertedItem.item, at: insertedItem.index) } let visibleIndices: [Int] = self.itemNodes.map { $0.index } var remapIndices: [Int: Int] = [:] for i in 0 ..< insertItems.count { if let previousIndex = insertItems[i].previousIndex, visibleIndices.contains(previousIndex) { remapIndices[previousIndex] = i } } for itemNode in self.itemNodes { if let remappedIndex = remapIndices[itemNode.index] { //print("remap visible node \(itemNode.index) -> \(remappedIndex)") itemNode.index = remappedIndex } } self.itemNodes.sort(by: { $0.index < $1.index }) //print("visible indices before update \(self.itemNodes.map { $0.index })") self.invalidatedItems = true if let focusOnItem = transaction.focusOnItem { self.centralItemIndex = focusOnItem } self.updateItemNodes(transition: .immediate, notify: transaction.focusOnItem != nil, synchronous: transaction.synchronous) //print("visible indices after update \(self.itemNodes.map { $0.index })") } else if let focusOnItem = transaction.focusOnItem { self.ignoreCentralItemIndexUpdate = true self.centralItemIndex = focusOnItem self.ignoreCentralItemIndexUpdate = false self.updateItemNodes(transition: .immediate, forceOffsetReset: true, synchronous: transaction.synchronous) } } func canGoToPreviousItem() -> Bool { if self.disableTapNavigation { return false } if let index = self.centralItemIndex, index > 0 { return true } else { return false } } func canGoToNextItem() -> Bool { if self.disableTapNavigation { return false } if let index = self.centralItemIndex, index < self.items.count - 1 { return true } else { return false } } func goToPreviousItem() { if let index = self.centralItemIndex, index > 0 { self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index - 1, synchronous: false)) } } func goToNextItem() { if let index = self.centralItemIndex, index < self.items.count - 1 { self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index + 1, synchronous: false)) } } private func makeNodeForItem(at index: Int, synchronous: Bool) -> GalleryItemNode { let node = self.items[index].node(synchronous: synchronous) node.toggleControlsVisibility = self.toggleControlsVisibility node.updateControlsVisibility = self.updateControlsVisibility node.updateOrientation = self.updateOrientation node.dismiss = self.dismiss node.beginCustomDismiss = self.beginCustomDismiss node.completeCustomDismiss = self.completeCustomDismiss node.baseNavigationController = self.baseNavigationController node.galleryController = self.galleryController node.index = index return node } private func visibleItemNode(at index: Int) -> GalleryItemNode? { for itemNode in self.itemNodes { if itemNode.index == index { return itemNode } } return nil } private func addVisibleItemNode(_ node: GalleryItemNode) { 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(transition: ContainedViewLayoutTransition, forceOffsetReset: Bool = false, notify: Bool = false, forceLoad: Bool = false, synchronous: Bool = false) { if self.items.isEmpty || self.containerLayout == nil { return } var resetOffsetToCentralItem = forceOffsetReset if let centralItemIndex = self.centralItemIndex, self.visibleItemNode(at: centralItemIndex) == nil, !self.itemNodes.isEmpty { repeat { self.removeVisibleItemNode(internalIndex: self.itemNodes.count - 1) } while self.itemNodes.count > 0 } if self.itemNodes.isEmpty { let node = self.makeNodeForItem(at: self.centralItemIndex ?? 0, synchronous: synchronous) node.frame = CGRect(origin: CGPoint(), size: self.scrollView.bounds.size) if let containerLayout = self.containerLayout { node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) } self.addVisibleItemNode(node) self.centralItemIndex = node.index resetOffsetToCentralItem = true } var notifyCentralItemUpdated = forceOffsetReset || notify if let centralItemIndex = self.centralItemIndex, let centralItemNode = self.visibleItemNode(at: centralItemIndex) { if centralItemIndex != 0 { if self.shouldLoadItems(force: forceLoad) && self.visibleItemNode(at: centralItemIndex - 1) == nil { let node = self.makeNodeForItem(at: centralItemIndex - 1, synchronous: synchronous) node.frame = centralItemNode.frame.offsetBy(dx: -centralItemNode.frame.size.width - self.pageGap, dy: 0.0) if let containerLayout = self.containerLayout { node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) } self.addVisibleItemNode(node) } } if centralItemIndex != self.items.count - 1 { if self.shouldLoadItems(force: forceLoad) && self.visibleItemNode(at: centralItemIndex + 1) == nil { let node = self.makeNodeForItem(at: centralItemIndex + 1, synchronous: synchronous) node.frame = centralItemNode.frame.offsetBy(dx: centralItemNode.frame.size.width + self.pageGap, dy: 0.0) if let containerLayout = self.containerLayout { node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) } self.addVisibleItemNode(node) } } for i in 0 ..< self.itemNodes.count { let node = self.itemNodes[i] transition.updateFrame(node: node, 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))) let screenFrame = node.view.convert(node.view.bounds, to: self.view.superview) node.screenFrameUpdated(screenFrame) } if resetOffsetToCentralItem { self.scrollView.contentOffset = CGPoint(x: centralItemNode.frame.minX - self.pageGap, y: 0.0) } if self.shouldLoadItems(force: forceLoad), 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.shouldLoadItems(force: forceLoad) && self.visibleItemNode(at: centralItemCandidateNode.index - 1) == nil { let node = self.makeNodeForItem(at: centralItemCandidateNode.index - 1, synchronous: synchronous) node.frame = centralItemCandidateNode.frame.offsetBy(dx: -centralItemCandidateNode.frame.size.width - self.pageGap, dy: 0.0) if let containerLayout = self.containerLayout { node.containerLayoutUpdated(containerLayout.0, navigationBarHeight: containerLayout.1, transition: .immediate) } self.addVisibleItemNode(node) } } if centralItemCandidateNode.index != items.count - 1 { if self.shouldLoadItems(force: forceLoad) && self.visibleItemNode(at: centralItemCandidateNode.index + 1) == nil { let node = self.makeNodeForItem(at: centralItemCandidateNode.index + 1, synchronous: synchronous) node.frame = centralItemCandidateNode.frame.offsetBy(dx: centralItemCandidateNode.frame.size.width + self.pageGap, dy: 0.0) if let containerLayout = 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 { let node = self.itemNodes[i] transition.updateFrame(node: node, 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))) let screenFrame = node.convert(node.bounds, to: self.supernode) node.screenFrameUpdated(screenFrame) } 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 { let isVisible = self.scrollView.bounds.intersects(itemNode.frame) itemNode.centralityUpdated(isCentral: itemNode.index == self.centralItemIndex) itemNode.visibilityUpdated(isVisible: isVisible) itemNode.isHidden = !isVisible } if notifyCentralItemUpdated { self.centralItemIndexUpdated(self.centralItemIndex) } self.updateCentralIndexOffset(transition: .immediate) } public func scrollViewDidScroll(_ scrollView: UIScrollView) { if !self.ignoreDidScroll { self.updateItemNodes(transition: .immediate) } } public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if !decelerate { self.ensureItemsLoaded(force: false) } } public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.ensureItemsLoaded(force: true) } public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.ensureItemsLoaded(force: true) } private func shouldLoadItems(force: Bool) -> Bool { return force || (!self.scrollView.isDecelerating && !self.scrollView.isDragging) } private func ensureItemsLoaded(force: Bool) { self.updateItemNodes(transition: .immediate, forceLoad: force) } private func centralItemCandidate() -> GalleryItemNode? { 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 } } private func updateCentralIndexOffset(transition: ContainedViewLayoutTransition) { if let centralIndex = self.centralItemIndex, let itemNode = self.visibleItemNode(at: centralIndex) { let offset: CGFloat = self.scrollView.contentOffset.x + self.pageGap - itemNode.frame.minX var progress = offset / self.scrollView.bounds.size.width progress = min(1.0, progress) progress = max(-1.0, progress) self.centralItemIndexOffsetUpdated((self.invalidatedItems ? self.items : nil, centralIndex, progress)) } else { self.invalidatedItems = false self.centralItemIndexOffsetUpdated(nil) } } public func forEachItemNode(_ f: (GalleryItemNode) -> Void) { for itemNode in self.itemNodes { f(itemNode) } } }