Swiftgram/submodules/GalleryUI/Sources/GalleryPagerNode.swift
2020-06-24 18:06:08 +03:00

663 lines
30 KiB
Swift

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))
}
private let leftFadeImage = generateImage(CGSize(width: 64.0, height: 1.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: 64.0, y: 0.0), options: CGGradientDrawingOptions())
})
private let rightFadeImage = generateImage(CGSize(width: 64.0, height: 1.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: 64.0, 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 scrollView: UIScrollView
private let leftFadeNode: ASImageNode
private let rightFadeNode: ASImageNode
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 dismiss: () -> Void = { }
public var beginCustomDismiss: () -> Void = { }
public var completeCustomDismiss: () -> Void = { }
public var baseNavigationController: () -> NavigationController? = { return nil }
public init(pageGap: CGFloat) {
self.pageGap = pageGap
self.scrollView = UIScrollView()
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
self.leftFadeNode = ASImageNode()
self.leftFadeNode.contentMode = .scaleToFill
self.leftFadeNode.image = leftFadeImage
self.leftFadeNode.alpha = 0.0
self.rightFadeNode = ASImageNode()
self.rightFadeNode.contentMode = .scaleToFill
self.rightFadeNode.image = rightFadeImage
self.rightFadeNode.alpha = 0.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)
}
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 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 {
highlightedSide = true
}
}
if highlightedSide == nil {
return .fail
}
if let result = strongSelf.hitTest(point, with: nil), let node = result.asyncdisplaykit_node as? ASButtonNode {
return .fail
}
return .keepWithSingleTap
}
recognizer.highlight = { [weak self] point in
guard let strongSelf = self 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))
}
let fadeWidth = min(72.0, layout.size.width * 0.2)
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<Void, NoError> {
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 func replaceItems(_ items: [GalleryItem], centralItemIndex: Int?, synchronous: Bool = false) {
var updateItems: [GalleryPagerUpdateItem] = []
var deleteItems: [Int] = []
var insertItems: [GalleryPagerInsertItem] = []
var previousIndexById: [AnyHashable: Int] = [:]
var 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)
}
}
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))
}
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)
}
}
private func canGoToPreviousItem() -> Bool {
if let index = self.centralItemIndex, index > 0 {
return true
} else {
return false
}
}
private func canGoToNextItem() -> Bool {
if let index = self.centralItemIndex, index < self.items.count - 1 {
return true
} else {
return false
}
}
private func goToPreviousItem() {
if let index = self.centralItemIndex, index > 0 {
self.transaction(GalleryPagerTransaction(deleteItems: [], insertItems: [], updateItems: [], focusOnItem: index - 1, synchronous: false))
}
}
private 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.dismiss = self.dismiss
node.beginCustomDismiss = self.beginCustomDismiss
node.completeCustomDismiss = self.completeCustomDismiss
node.baseNavigationController = self.baseNavigationController
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: 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.convert(node.bounds, to: self.supernode)
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)
}
}
}