mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
no message
This commit is contained in:
parent
684ab193c8
commit
a3bbfd59b3
@ -13,6 +13,10 @@
|
||||
D015F7541D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7531D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift */; };
|
||||
D015F7581D1B467200E269B5 /* ActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7571D1B467200E269B5 /* ActionSheetController.swift */; };
|
||||
D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7591D1B46B600E269B5 /* ActionSheetControllerNode.swift */; };
|
||||
D01E2BDE1D9049620066BF65 /* GridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BDD1D9049620066BF65 /* GridNode.swift */; };
|
||||
D01E2BE01D90498E0066BF65 /* GridNodeScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */; };
|
||||
D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BE11D9049F60066BF65 /* GridItemNode.swift */; };
|
||||
D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E2BE31D904A000066BF65 /* GridItem.swift */; };
|
||||
D02958001D6F096000360E5E /* ContextMenuContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */; };
|
||||
D02BDB021B6AC703008AFAD2 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */; };
|
||||
D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C01D6DF594007FC290 /* ContextMenuNode.swift */; };
|
||||
@ -120,6 +124,10 @@
|
||||
D015F7531D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemContainedControllerTransitionCoordinator.swift; sourceTree = "<group>"; };
|
||||
D015F7571D1B467200E269B5 /* ActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetController.swift; sourceTree = "<group>"; };
|
||||
D015F7591D1B46B600E269B5 /* ActionSheetControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetControllerNode.swift; sourceTree = "<group>"; };
|
||||
D01E2BDD1D9049620066BF65 /* GridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridNode.swift; sourceTree = "<group>"; };
|
||||
D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridNodeScroller.swift; sourceTree = "<group>"; };
|
||||
D01E2BE11D9049F60066BF65 /* GridItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItemNode.swift; sourceTree = "<group>"; };
|
||||
D01E2BE31D904A000066BF65 /* GridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItem.swift; sourceTree = "<group>"; };
|
||||
D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuContainerNode.swift; sourceTree = "<group>"; };
|
||||
D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuntimeUtils.swift; sourceTree = "<group>"; };
|
||||
D03725C01D6DF594007FC290 /* ContextMenuNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuNode.swift; sourceTree = "<group>"; };
|
||||
@ -269,6 +277,26 @@
|
||||
name = "Action Sheet";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D01E2BDC1D90494A0066BF65 /* Grid Node */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D01E2BDD1D9049620066BF65 /* GridNode.swift */,
|
||||
D01E2BDF1D90498E0066BF65 /* GridNodeScroller.swift */,
|
||||
D01E2BE31D904A000066BF65 /* GridItem.swift */,
|
||||
D01E2BE11D9049F60066BF65 /* GridItemNode.swift */,
|
||||
);
|
||||
name = "Grid Node";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D01E2BE51D904A530066BF65 /* Collection Nodes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0C2DFBA1CC443080044FF83 /* List Node */,
|
||||
D01E2BDC1D90494A0066BF65 /* Grid Node */,
|
||||
);
|
||||
name = "Collection Nodes";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D02BDAEC1B6A7053008AFAD2 /* Nodes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -340,7 +368,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D08122991D19A9E0005F7395 /* User Interface */,
|
||||
D0C2DFBA1CC443080044FF83 /* List View */,
|
||||
D01E2BE51D904A530066BF65 /* Collection Nodes */,
|
||||
D03BCCE91C72AE4B0097A291 /* Theme */,
|
||||
D05CC3001B6955D500E235A3 /* Utils */,
|
||||
D07921AA1B6FC911005C23D9 /* Status Bar */,
|
||||
@ -483,7 +511,7 @@
|
||||
name = Controllers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0C2DFBA1CC443080044FF83 /* List View */ = {
|
||||
D0C2DFBA1CC443080044FF83 /* List Node */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0C2DFBB1CC4431D0044FF83 /* ASTransformLayerNode.swift */,
|
||||
@ -497,7 +525,7 @@
|
||||
D0C2DFC41CC4431D0044FF83 /* ListViewScroller.swift */,
|
||||
D0C2DFC51CC4431D0044FF83 /* ListViewAccessoryItemNode.swift */,
|
||||
);
|
||||
name = "List View";
|
||||
name = "List Node";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0DC48521BF93D7C00F672FD /* Tabs */ = {
|
||||
@ -661,6 +689,8 @@
|
||||
D03E7DFF1C96F7B400C07816 /* StatusBarManager.swift in Sources */,
|
||||
D05CC3161B695A9600E235A3 /* NavigationBar.swift in Sources */,
|
||||
D05CC31D1B695A9600E235A3 /* UIBarButtonItem+Proxy.m in Sources */,
|
||||
D01E2BDE1D9049620066BF65 /* GridNode.swift in Sources */,
|
||||
D01E2BE01D90498E0066BF65 /* GridNodeScroller.swift in Sources */,
|
||||
D0C85DD61D1C600D00124894 /* ActionSheetButtonNode.swift in Sources */,
|
||||
D0C2DFD01CC4431D0044FF83 /* ListViewAccessoryItemNode.swift in Sources */,
|
||||
D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */,
|
||||
@ -691,8 +721,10 @@
|
||||
D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */,
|
||||
D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */,
|
||||
D053CB611D22B4F200DD41DF /* CATracingLayer.m in Sources */,
|
||||
D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */,
|
||||
D081229D1D19AA1C005F7395 /* ContainerViewLayout.swift in Sources */,
|
||||
D0C2DFC71CC4431D0044FF83 /* ListViewItemNode.swift in Sources */,
|
||||
D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */,
|
||||
D08E903A1D24159200533158 /* ActionSheetItem.swift in Sources */,
|
||||
D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */,
|
||||
D05CC2EC1B69558A00E235A3 /* RuntimeUtils.m in Sources */,
|
||||
|
@ -83,6 +83,28 @@ public extension CALayer {
|
||||
self.add(animation, forKey: keyPath)
|
||||
}
|
||||
}
|
||||
|
||||
public func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, initialVelocity: CGFloat = 0.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
let animation = makeSpringBounceAnimation(keyPath, initialVelocity)
|
||||
animation.fromValue = from
|
||||
animation.toValue = to
|
||||
animation.isRemovedOnCompletion = removeOnCompletion
|
||||
animation.fillMode = kCAFillModeForwards
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(completion: completion)
|
||||
}
|
||||
|
||||
let k = Float(UIView.animationDurationFactor())
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
animation.speed = speed * Float(animation.duration / duration)
|
||||
animation.isAdditive = additive
|
||||
|
||||
self.add(animation, forKey: keyPath)
|
||||
}
|
||||
|
||||
public func animateAdditive(from: NSValue, to: NSValue, keyPath: String, key: String, timingFunction: String, duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
let k = Float(UIView.animationDurationFactor())
|
||||
@ -115,7 +137,7 @@ public extension CALayer {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
}
|
||||
|
||||
func animatePosition(from: CGPoint, to: CGPoint, duration: Double, timingFunction: String, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
func animatePosition(from: CGPoint, to: CGPoint, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if from == to {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
@ -146,7 +168,29 @@ public extension CALayer {
|
||||
}
|
||||
return
|
||||
}
|
||||
self.animatePosition(from: CGPoint(x: from.midX, y: from.midY), to: CGPoint(x: to.midX, y: to.midY), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: nil)
|
||||
self.animateBounds(from: CGRect(origin: self.bounds.origin, size: from.size), to: CGRect(origin: self.bounds.origin, size: to.size), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||
var interrupted = false
|
||||
var completedPosition = false
|
||||
var completedBounds = false
|
||||
var partialCompletion: () -> Void = {
|
||||
if interrupted || (completedPosition && completedBounds) {
|
||||
if let completion = completion {
|
||||
completion(!interrupted)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.animatePosition(from: CGPoint(x: from.midX, y: from.midY), to: CGPoint(x: to.midX, y: to.midY), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { value in
|
||||
if !value {
|
||||
interrupted = true
|
||||
}
|
||||
completedPosition = true
|
||||
partialCompletion()
|
||||
})
|
||||
self.animateBounds(from: CGRect(origin: self.bounds.origin, size: from.size), to: CGRect(origin: self.bounds.origin, size: to.size), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { value in
|
||||
if !value {
|
||||
interrupted = true
|
||||
}
|
||||
completedBounds = true
|
||||
partialCompletion()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ final class ContextMenuNode: ASDisplayNode {
|
||||
private let actionNodes: [ContextMenuActionNode]
|
||||
|
||||
var sourceRect: CGRect?
|
||||
var arrowOnBottom: Bool = true
|
||||
|
||||
private var dismissedByTouchOutside = false
|
||||
|
||||
@ -59,6 +60,7 @@ final class ContextMenuNode: ASDisplayNode {
|
||||
verticalOrigin = min(layout.size.height - insets.bottom - 54.0, sourceRect.maxY)
|
||||
arrowOnBottom = false
|
||||
}
|
||||
self.arrowOnBottom = arrowOnBottom
|
||||
|
||||
let horizontalOrigin: CGFloat = floor(min(max(8.0, sourceRect.midX - actionsWidth / 2.0), layout.size.width - actionsWidth - 8.0))
|
||||
|
||||
@ -69,7 +71,12 @@ final class ContextMenuNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.2)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4)
|
||||
|
||||
let containerPosition = self.containerNode.layer.position
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: containerPosition.x, y: containerPosition.y + (self.arrowOnBottom ? 1.0 : -1.0) * self.containerNode.bounds.size.height / 2.0)), to: NSValue(cgPoint: containerPosition), keyPath: "position", duration: 0.4)
|
||||
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
|
5
Display/GridItem.swift
Normal file
5
Display/GridItem.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
public protocol GridItem {
|
||||
func node(layout: GridNodeLayout) -> GridItemNode
|
||||
}
|
6
Display/GridItemNode.swift
Normal file
6
Display/GridItemNode.swift
Normal file
@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
|
||||
open class GridItemNode: ASDisplayNode {
|
||||
|
||||
}
|
388
Display/GridNode.swift
Normal file
388
Display/GridNode.swift
Normal file
@ -0,0 +1,388 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
|
||||
public struct GridNodeInsertItem {
|
||||
public let index: Int
|
||||
public let item: GridItem
|
||||
public let previousIndex: Int?
|
||||
|
||||
public init(index: Int, item: GridItem, previousIndex: Int?) {
|
||||
self.index = index
|
||||
self.item = item
|
||||
self.previousIndex = previousIndex
|
||||
}
|
||||
}
|
||||
|
||||
public struct GridNodeUpdateItem {
|
||||
public let index: Int
|
||||
public let item: GridItem
|
||||
|
||||
public init(index: Int, item: GridItem) {
|
||||
self.index = index
|
||||
self.item = item
|
||||
}
|
||||
}
|
||||
|
||||
public enum GridNodeScrollToItemPosition {
|
||||
case top
|
||||
case bottom
|
||||
case center
|
||||
}
|
||||
|
||||
public struct GridNodeScrollToItem {
|
||||
public let index: Int
|
||||
public let position: GridNodeScrollToItemPosition
|
||||
|
||||
public init(index: Int, position: GridNodeScrollToItemPosition) {
|
||||
self.index = index
|
||||
self.position = position
|
||||
}
|
||||
}
|
||||
|
||||
public struct GridNodeLayout: Equatable {
|
||||
public let size: CGSize
|
||||
public let insets: UIEdgeInsets
|
||||
public let preloadSize: CGFloat
|
||||
public let itemSize: CGSize
|
||||
public let indexOffset: Int
|
||||
|
||||
public init(size: CGSize, insets: UIEdgeInsets, preloadSize: CGFloat, itemSize: CGSize, indexOffset: Int) {
|
||||
self.size = size
|
||||
self.insets = insets
|
||||
self.preloadSize = preloadSize
|
||||
self.itemSize = itemSize
|
||||
self.indexOffset = indexOffset
|
||||
}
|
||||
|
||||
public static func ==(lhs: GridNodeLayout, rhs: GridNodeLayout) -> Bool {
|
||||
return lhs.size.equalTo(rhs.size) && lhs.insets == rhs.insets && lhs.preloadSize.isEqual(to: rhs.preloadSize) && lhs.itemSize.equalTo(rhs.itemSize) && lhs.indexOffset == rhs.indexOffset
|
||||
}
|
||||
}
|
||||
|
||||
public struct GridNodeUpdateLayout {
|
||||
public let layout: GridNodeLayout
|
||||
public let transition: ContainedViewLayoutTransition
|
||||
|
||||
public init(layout: GridNodeLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.layout = layout
|
||||
self.transition = transition
|
||||
}
|
||||
}
|
||||
|
||||
/*private func binarySearch(_ inputArr: [GridNodePresentationItem], searchItem: CGFloat) -> Int? {
|
||||
if inputArr.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
var lowerPosition = inputArr[0].frame.origin.y + inputArr[0].frame.size.height
|
||||
var upperPosition = inputArr[inputArr.count - 1].frame.origin.y
|
||||
|
||||
if lowerPosition > upperPosition {
|
||||
return nil
|
||||
}
|
||||
|
||||
while (true) {
|
||||
let currentPosition = (lowerIndex + upperIndex) / 2
|
||||
if (inputArr[currentIndex] == searchItem) {
|
||||
return currentIndex
|
||||
} else if (lowerIndex > upperIndex) {
|
||||
return nil
|
||||
} else {
|
||||
if (inputArr[currentIndex] > searchItem) {
|
||||
upperIndex = currentIndex - 1
|
||||
} else {
|
||||
lowerIndex = currentIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
public struct GridNodeTransaction {
|
||||
public let deleteItems: [Int]
|
||||
public let insertItems: [GridNodeInsertItem]
|
||||
public let updateItems: [GridNodeUpdateItem]
|
||||
public let scrollToItem: GridNodeScrollToItem?
|
||||
public let updateLayout: GridNodeUpdateLayout?
|
||||
public let stationaryItemRange: (Int, Int)?
|
||||
|
||||
public init(deleteItems: [Int], insertItems: [GridNodeInsertItem], updateItems: [GridNodeUpdateItem], scrollToItem: GridNodeScrollToItem?, updateLayout: GridNodeUpdateLayout?, stationaryItemRange: (Int, Int)?) {
|
||||
self.deleteItems = deleteItems
|
||||
self.insertItems = insertItems
|
||||
self.updateItems = updateItems
|
||||
self.scrollToItem = scrollToItem
|
||||
self.updateLayout = updateLayout
|
||||
self.stationaryItemRange = stationaryItemRange
|
||||
}
|
||||
}
|
||||
|
||||
private struct GridNodePresentationItem {
|
||||
let index: Int
|
||||
let frame: CGRect
|
||||
}
|
||||
|
||||
private struct GridNodePresentationLayout {
|
||||
let layout: GridNodeLayout
|
||||
let contentOffset: CGPoint
|
||||
let contentSize: CGSize
|
||||
let items: [GridNodePresentationItem]
|
||||
}
|
||||
|
||||
private final class GridNodeItemLayout {
|
||||
let contentSize: CGSize
|
||||
let items: [GridNodePresentationItem]
|
||||
|
||||
init(contentSize: CGSize, items: [GridNodePresentationItem]) {
|
||||
self.contentSize = contentSize
|
||||
self.items = items
|
||||
}
|
||||
}
|
||||
|
||||
public struct GridNodeDisplayedItemRange: Equatable {
|
||||
public let loadedRange: Range<Int>?
|
||||
public let visibleRange: Range<Int>?
|
||||
|
||||
public static func ==(lhs: GridNodeDisplayedItemRange, rhs: GridNodeDisplayedItemRange) -> Bool {
|
||||
return lhs.loadedRange == rhs.loadedRange && lhs.visibleRange == rhs.visibleRange
|
||||
}
|
||||
}
|
||||
|
||||
open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
private var gridLayout = GridNodeLayout(size: CGSize(), insets: UIEdgeInsets(), preloadSize: 0.0, itemSize: CGSize(), indexOffset: 0)
|
||||
private var items: [GridItem] = []
|
||||
private var itemNodes: [Int: GridItemNode] = [:]
|
||||
private var itemLayout = GridNodeItemLayout(contentSize: CGSize(), items: [])
|
||||
|
||||
private var applyingContentOffset = false
|
||||
|
||||
public override init() {
|
||||
super.init()
|
||||
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.delegate = self
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func transaction(_ transaction: GridNodeTransaction, completion: (GridNodeDisplayedItemRange) -> Void) {
|
||||
if transaction.deleteItems.isEmpty && transaction.insertItems.isEmpty && transaction.scrollToItem == nil && transaction.updateItems.isEmpty && (transaction.updateLayout == nil || transaction.updateLayout!.layout == self.gridLayout) {
|
||||
completion(self.displayedItemRange())
|
||||
return
|
||||
}
|
||||
|
||||
if let updateLayout = transaction.updateLayout {
|
||||
self.gridLayout = updateLayout.layout
|
||||
}
|
||||
|
||||
for updatedItem in transaction.updateItems {
|
||||
self.items[updatedItem.index] = updatedItem.item
|
||||
if let itemNode = self.itemNodes[updatedItem.index] {
|
||||
//update node
|
||||
}
|
||||
}
|
||||
|
||||
if !transaction.deleteItems.isEmpty || !transaction.insertItems.isEmpty {
|
||||
let deleteItems = transaction.deleteItems.sorted()
|
||||
|
||||
for deleteItemIndex in deleteItems.reversed() {
|
||||
self.items.remove(at: deleteItemIndex)
|
||||
self.removeItemNodeWithIndex(deleteItemIndex)
|
||||
}
|
||||
|
||||
var remappedDeletionItemNodes: [Int: GridItemNode] = [:]
|
||||
|
||||
for (index, itemNode) in self.itemNodes {
|
||||
var indexOffset = 0
|
||||
for deleteIndex in deleteItems {
|
||||
if deleteIndex < index {
|
||||
indexOffset += 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
remappedDeletionItemNodes[index - indexOffset] = itemNode
|
||||
}
|
||||
|
||||
let insertItems = transaction.insertItems.sorted(by: { $0.index < $1.index })
|
||||
if self.items.count == 0 && !insertItems.isEmpty {
|
||||
if insertItems[0].index != 0 {
|
||||
fatalError("transaction: invalid insert into empty list")
|
||||
}
|
||||
}
|
||||
|
||||
for insertedItem in insertItems {
|
||||
self.items.insert(insertedItem.item, at: insertedItem.index)
|
||||
}
|
||||
|
||||
var remappedInsertionItemNodes: [Int: GridItemNode] = [:]
|
||||
for (index, itemNode) in remappedDeletionItemNodes {
|
||||
var indexOffset = 0
|
||||
for insertedItem in transaction.insertItems {
|
||||
if insertedItem.index <= index + indexOffset {
|
||||
indexOffset += 1
|
||||
}
|
||||
}
|
||||
|
||||
remappedInsertionItemNodes[index + indexOffset] = itemNode
|
||||
}
|
||||
|
||||
self.itemNodes = remappedInsertionItemNodes
|
||||
}
|
||||
|
||||
self.itemLayout = self.generateItemLayout()
|
||||
|
||||
self.applyPresentaionLayout(self.generatePresentationLayout(scrollToItemIndex: 0))
|
||||
|
||||
completion(self.displayedItemRange())
|
||||
}
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.applyingContentOffset {
|
||||
self.applyPresentaionLayout(self.generatePresentationLayout())
|
||||
}
|
||||
}
|
||||
|
||||
private func displayedItemRange() -> GridNodeDisplayedItemRange {
|
||||
var minIndex: Int?
|
||||
var maxIndex: Int?
|
||||
for index in self.itemNodes.keys {
|
||||
if minIndex == nil || minIndex! > index {
|
||||
minIndex = index
|
||||
}
|
||||
if maxIndex == nil || maxIndex! < index {
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
if let minIndex = minIndex, let maxIndex = maxIndex {
|
||||
return GridNodeDisplayedItemRange(loadedRange: minIndex ..< maxIndex, visibleRange: minIndex ..< maxIndex)
|
||||
} else {
|
||||
return GridNodeDisplayedItemRange(loadedRange: nil, visibleRange: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func generateItemLayout() -> GridNodeItemLayout {
|
||||
if CGFloat(0.0).isLess(than: gridLayout.size.width) && CGFloat(0.0).isLess(than: gridLayout.size.height) && !self.items.isEmpty {
|
||||
var contentSize = CGSize(width: gridLayout.size.width, height: 0.0)
|
||||
var items: [GridNodePresentationItem] = []
|
||||
|
||||
var incrementedCurrentRow = false
|
||||
var nextItemOrigin = CGPoint(x: 0.0, y: 0.0)
|
||||
var index = 0
|
||||
for item in self.items {
|
||||
if !incrementedCurrentRow {
|
||||
incrementedCurrentRow = true
|
||||
contentSize.height += gridLayout.itemSize.height
|
||||
}
|
||||
|
||||
items.append(GridNodePresentationItem(index: index, frame: CGRect(origin: nextItemOrigin, size: gridLayout.itemSize)))
|
||||
index += 1
|
||||
|
||||
nextItemOrigin.x += gridLayout.itemSize.width
|
||||
if nextItemOrigin.x + gridLayout.itemSize.width > gridLayout.size.width {
|
||||
nextItemOrigin.x = 0.0
|
||||
nextItemOrigin.y += gridLayout.itemSize.height
|
||||
incrementedCurrentRow = false
|
||||
}
|
||||
}
|
||||
|
||||
return GridNodeItemLayout(contentSize: contentSize, items: items)
|
||||
} else {
|
||||
return GridNodeItemLayout(contentSize: CGSize(), items: [])
|
||||
}
|
||||
}
|
||||
|
||||
private func generatePresentationLayout(scrollToItemIndex: Int? = nil) -> GridNodePresentationLayout {
|
||||
if CGFloat(0.0).isLess(than: gridLayout.size.width) && CGFloat(0.0).isLess(than: gridLayout.size.height) && !self.itemLayout.items.isEmpty {
|
||||
let contentOffset: CGPoint
|
||||
if let scrollToItemIndex = scrollToItemIndex {
|
||||
let itemFrame = self.itemLayout.items[scrollToItemIndex]
|
||||
|
||||
let displayHeight = max(0.0, self.gridLayout.size.height - self.gridLayout.insets.top - self.gridLayout.insets.bottom)
|
||||
var verticalOffset = floor(itemFrame.frame.minY + itemFrame.frame.size.height / 2.0 - displayHeight / 2.0 - self.gridLayout.insets.top)
|
||||
|
||||
if verticalOffset > self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height {
|
||||
verticalOffset = self.itemLayout.contentSize.height + self.gridLayout.insets.bottom - self.gridLayout.size.height
|
||||
}
|
||||
if verticalOffset < -self.gridLayout.insets.top {
|
||||
verticalOffset = -self.gridLayout.insets.top
|
||||
}
|
||||
|
||||
contentOffset = CGPoint(x: 0.0, y: verticalOffset)
|
||||
} else {
|
||||
contentOffset = self.scrollView.contentOffset
|
||||
}
|
||||
|
||||
let lowerDisplayBound = contentOffset.y - self.gridLayout.preloadSize
|
||||
let upperDisplayBound = contentOffset.y + self.gridLayout.size.height + self.gridLayout.preloadSize
|
||||
|
||||
var presentationItems: [GridNodePresentationItem] = []
|
||||
for item in self.itemLayout.items {
|
||||
if item.frame.origin.y < lowerDisplayBound {
|
||||
continue
|
||||
}
|
||||
if item.frame.origin.y + item.frame.size.height > upperDisplayBound {
|
||||
break
|
||||
}
|
||||
presentationItems.append(item)
|
||||
}
|
||||
|
||||
return GridNodePresentationLayout(layout: self.gridLayout, contentOffset: contentOffset, contentSize: self.itemLayout.contentSize, items: presentationItems)
|
||||
} else {
|
||||
return GridNodePresentationLayout(layout: self.gridLayout, contentOffset: CGPoint(), contentSize: self.itemLayout.contentSize, items: [])
|
||||
}
|
||||
}
|
||||
|
||||
private func applyPresentaionLayout(_ presentationLayout: GridNodePresentationLayout) {
|
||||
applyingContentOffset = true
|
||||
self.scrollView.contentSize = presentationLayout.contentSize
|
||||
self.scrollView.contentInset = presentationLayout.layout.insets
|
||||
if !self.scrollView.contentOffset.equalTo(presentationLayout.contentOffset) {
|
||||
self.scrollView.setContentOffset(presentationLayout.contentOffset, animated: false)
|
||||
}
|
||||
applyingContentOffset = false
|
||||
|
||||
var existingItemIndices = Set<Int>()
|
||||
for item in presentationLayout.items {
|
||||
existingItemIndices.insert(item.index)
|
||||
|
||||
if let itemNode = self.itemNodes[item.index] {
|
||||
itemNode.frame = item.frame
|
||||
} else {
|
||||
let itemNode = self.items[item.index].node(layout: presentationLayout.layout)
|
||||
itemNode.frame = item.frame
|
||||
self.addItemNode(index: item.index, itemNode: itemNode)
|
||||
}
|
||||
}
|
||||
|
||||
for index in self.itemNodes.keys {
|
||||
if !existingItemIndices.contains(index) {
|
||||
self.removeItemNodeWithIndex(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addItemNode(index: Int, itemNode: GridItemNode) {
|
||||
assert(self.itemNodes[index] == nil)
|
||||
self.itemNodes[index] = itemNode
|
||||
if itemNode.supernode == nil {
|
||||
self.addSubnode(itemNode)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeItemNodeWithIndex(_ index: Int) {
|
||||
if let itemNode = self.itemNodes.removeValue(forKey: index) {
|
||||
itemNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
public func forEachItemNode(_ f: @noescape(ASDisplayNode) -> Void) {
|
||||
for (_, node) in self.itemNodes {
|
||||
f(node)
|
||||
}
|
||||
}
|
||||
}
|
29
Display/GridNodeScroller.swift
Normal file
29
Display/GridNodeScroller.swift
Normal file
@ -0,0 +1,29 @@
|
||||
import UIKit
|
||||
|
||||
private class GridNodeScrollerView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
open class GridNodeScroller: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
var scrollView: UIScrollView {
|
||||
return self.view as! UIScrollView
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init(viewBlock: {
|
||||
return GridNodeScrollerView()
|
||||
}, didLoad: nil)
|
||||
|
||||
self.scrollView.scrollsToTop = false
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
@ -99,11 +99,13 @@ public struct ListViewInsertItem {
|
||||
|
||||
public struct ListViewUpdateItem {
|
||||
public let index: Int
|
||||
public let previousIndex: Int
|
||||
public let item: ListViewItem
|
||||
public let directionHint: ListViewItemOperationDirectionHint?
|
||||
|
||||
public init(index: Int, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?) {
|
||||
public init(index: Int, previousIndex: Int, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?) {
|
||||
self.index = index
|
||||
self.previousIndex = previousIndex
|
||||
self.item = item
|
||||
self.directionHint = directionHint
|
||||
}
|
||||
@ -579,8 +581,9 @@ private struct ListViewState {
|
||||
while i >= 0 {
|
||||
let itemNode = self.nodes[i]
|
||||
let frame = itemNode.frame
|
||||
//print("node \(i) frame \(frame)")
|
||||
if frame.maxY < -self.invisibleInset || frame.origin.y > self.visibleSize.height + self.invisibleInset {
|
||||
//print("remove \(i)")
|
||||
//print("remove invisible 1 \(i) frame \(frame)")
|
||||
operations.append(.Remove(index: i, offsetDirection: frame.maxY < -self.invisibleInset ? .Down : .Up))
|
||||
self.nodes.remove(at: i)
|
||||
}
|
||||
@ -599,7 +602,7 @@ private struct ListViewState {
|
||||
if self.nodes[j].frame.maxY < upperBound {
|
||||
if let index = self.nodes[j].index {
|
||||
if index != previousIndex - 1 {
|
||||
print("remove monotonity \(j) (\(index))")
|
||||
//print("remove monotonity \(j) (\(index))")
|
||||
operations.append(.Remove(index: j, offsetDirection: .Down))
|
||||
self.nodes.remove(at: j)
|
||||
} else {
|
||||
@ -633,7 +636,7 @@ private struct ListViewState {
|
||||
}
|
||||
if !removeIndices.isEmpty {
|
||||
for i in removeIndices.reversed() {
|
||||
print("remove monotonity \(i) (\(self.nodes[i].index!))")
|
||||
//print("remove monotonity \(i) (\(self.nodes[i].index!))")
|
||||
operations.append(.Remove(index: i, offsetDirection: .Up))
|
||||
self.nodes.remove(at: i)
|
||||
}
|
||||
@ -776,7 +779,7 @@ private struct ListViewState {
|
||||
|
||||
if let referenceNode = referenceNode , animated {
|
||||
self.nodes.insert(.Placeholder(frame: nodeFrame), at: index)
|
||||
operations.append(.InsertPlaceholder(index: index, referenceNode: referenceNode, offsetDirection: offsetDirection.inverted()))
|
||||
operations.append(.InsertDisappearingPlaceholder(index: index, referenceNode: referenceNode, offsetDirection: offsetDirection.inverted()))
|
||||
} else {
|
||||
if nodeFrame.maxY > self.insets.top - CGFloat(FLT_EPSILON) {
|
||||
if let direction = direction , direction == .Down && node.frame.minY < self.visibleSize.height - self.insets.bottom + CGFloat(FLT_EPSILON) {
|
||||
@ -849,9 +852,9 @@ private struct ListViewState {
|
||||
}
|
||||
}
|
||||
|
||||
operations.append(.UpdateLayout(index: itemIndex, layout: layout, apply: apply))
|
||||
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
|
||||
case .System:
|
||||
operations.append(.UpdateLayout(index: itemIndex, layout: layout, apply: apply))
|
||||
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
|
||||
}
|
||||
|
||||
break
|
||||
@ -862,7 +865,7 @@ private struct ListViewState {
|
||||
|
||||
private enum ListViewStateOperation {
|
||||
case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> ())
|
||||
case InsertPlaceholder(index: Int, referenceNode: ListViewItemNode, offsetDirection: ListViewInsertionOffsetDirection)
|
||||
case InsertDisappearingPlaceholder(index: Int, referenceNode: ListViewItemNode, offsetDirection: ListViewInsertionOffsetDirection)
|
||||
case Remove(index: Int, offsetDirection: ListViewInsertionOffsetDirection)
|
||||
case Remap([Int: Int])
|
||||
case UpdateLayout(index: Int, layout: ListViewItemNodeLayout, apply: () -> ())
|
||||
@ -928,7 +931,7 @@ public enum ListViewVisibleContentOffset {
|
||||
case none
|
||||
}
|
||||
|
||||
public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
private final let scroller: ListViewScroller
|
||||
private final var visibleSize: CGSize = CGSize()
|
||||
private final var insets = UIEdgeInsets()
|
||||
@ -1038,6 +1041,10 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
self.scroller.panGestureRecognizer.cancelsTouchesInView = true
|
||||
self.view.addGestureRecognizer(self.scroller.panGestureRecognizer)
|
||||
|
||||
let trackingRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.trackingGesture(_:)))
|
||||
trackingRecognizer.delegate = self
|
||||
self.view.addGestureRecognizer(trackingRecognizer)
|
||||
|
||||
self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
|
||||
self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
|
||||
if #available(iOS 10.0, *) {
|
||||
@ -1181,17 +1188,18 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
var useScrollDynamics = false
|
||||
|
||||
let anchor: CGFloat
|
||||
if self.isTracking {
|
||||
anchor = self.touchesPosition.y
|
||||
} else if deltaY < 0.0 {
|
||||
anchor = self.visibleSize.height
|
||||
} else {
|
||||
anchor = 0.0
|
||||
}
|
||||
|
||||
for itemNode in self.itemNodes {
|
||||
if itemNode.wantsScrollDynamics {
|
||||
useScrollDynamics = true
|
||||
let anchor: CGFloat
|
||||
if self.isTracking {
|
||||
anchor = self.touchesPosition.y
|
||||
} else if deltaY < 0.0 {
|
||||
anchor = self.visibleSize.height
|
||||
} else {
|
||||
anchor = 0.0
|
||||
}
|
||||
|
||||
var distance: CGFloat
|
||||
let itemFrame = itemNode.apparentFrame
|
||||
@ -1443,11 +1451,19 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
private func deleteAndInsertItemsTransaction(deleteIndices: [ListViewDeleteItem], insertIndicesAndItems: [ListViewInsertItem], updateIndicesAndItems: [ListViewUpdateItem], options: ListViewDeleteAndInsertOptions, scrollToItem: ListViewScrollToItem?, updateSizeAndInsets: ListViewUpdateSizeAndInsets?, stationaryItemRange: (Int, Int)?, completion: @escaping (Void) -> Void) {
|
||||
if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && scrollToItem == nil {
|
||||
if deleteIndices.isEmpty && insertIndicesAndItems.isEmpty && updateIndicesAndItems.isEmpty && scrollToItem == nil {
|
||||
if let updateSizeAndInsets = updateSizeAndInsets , self.items.count == 0 || (updateSizeAndInsets.size == self.visibleSize && updateSizeAndInsets.insets == self.insets) {
|
||||
self.visibleSize = updateSizeAndInsets.size
|
||||
self.insets = updateSizeAndInsets.insets
|
||||
|
||||
if useDynamicTuning {
|
||||
let size = updateSizeAndInsets.size
|
||||
self.frictionSlider.frame = CGRect(x: 10.0, y: size.height - insets.bottom - 10.0 - self.frictionSlider.bounds.height, width: size.width - 20.0, height: self.frictionSlider.bounds.height)
|
||||
self.springSlider.frame = CGRect(x: 10.0, y: self.frictionSlider.frame.minY - self.springSlider.bounds.height, width: size.width - 20.0, height: self.springSlider.bounds.height)
|
||||
self.freeResistanceSlider.frame = CGRect(x: 10.0, y: self.springSlider.frame.minY - self.freeResistanceSlider.bounds.height, width: size.width - 20.0, height: self.freeResistanceSlider.bounds.height)
|
||||
self.scrollingResistanceSlider.frame = CGRect(x: 10.0, y: self.freeResistanceSlider.frame.minY - self.scrollingResistanceSlider.bounds.height, width: size.width - 20.0, height: self.scrollingResistanceSlider.bounds.height)
|
||||
}
|
||||
|
||||
self.ignoreScrollingEvents = true
|
||||
self.scroller.frame = CGRect(origin: CGPoint(), size: updateSizeAndInsets.size)
|
||||
self.scroller.contentSize = CGSize(width: updateSizeAndInsets.size.width, height: infiniteScrollSize * 2.0)
|
||||
@ -1491,17 +1507,6 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if self.debugInfo {
|
||||
//print("deleteAndInsertItemsTransaction deleteIndices: \(deleteIndices.map({$0.index})) insertIndicesAndItems: \(insertIndicesAndItems.map({"\($0.index) <- \($0.previousIndex)"}))")
|
||||
}
|
||||
|
||||
/*if scrollToItem != nil {
|
||||
print("Current indices:")
|
||||
for itemNode in self.itemNodes {
|
||||
print(" \(itemNode.index)")
|
||||
}
|
||||
}*/
|
||||
|
||||
var previousNodes: [Int: ListViewItemNode] = [:]
|
||||
for insertedItem in sortedIndicesAndItems {
|
||||
self.items.insert(insertedItem.item, at: insertedItem.index)
|
||||
@ -1517,8 +1522,9 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
for updatedItem in updateIndicesAndItems {
|
||||
self.items[updatedItem.index] = updatedItem.item
|
||||
for itemNode in self.itemNodes {
|
||||
if itemNode.index == updatedItem.index {
|
||||
if itemNode.index == updatedItem.previousIndex {
|
||||
previousNodes[updatedItem.index] = itemNode
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1750,12 +1756,23 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
var updatedState = state
|
||||
var updatedOperations = operations
|
||||
|
||||
let heightDelta = layout.size.height - updatedState.nodes[i].frame.size.height
|
||||
|
||||
updatedOperations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
|
||||
|
||||
if nodeIndex + 1 < updatedState.nodes.count {
|
||||
for i in nodeIndex + 1 ..< updatedState.nodes.count {
|
||||
let frame = updatedState.nodes[i].frame
|
||||
updatedState.nodes[i].frame = frame.offsetBy(dx: 0.0, dy: frame.size.height)
|
||||
if !animated {
|
||||
let previousFrame = updatedState.nodes[i].frame
|
||||
updatedState.nodes[i].frame = CGRect(origin: previousFrame.origin, size: layout.size)
|
||||
if previousFrame.minY < updatedState.insets.top {
|
||||
for j in 0 ... i {
|
||||
updatedState.nodes[j].frame = updatedState.nodes[j].frame.offsetBy(dx: 0.0, dy: -heightDelta)
|
||||
}
|
||||
} else {
|
||||
if i != updatedState.nodes.count {
|
||||
for j in i + 1 ..< updatedState.nodes.count {
|
||||
updatedState.nodes[j].frame = updatedState.nodes[j].frame.offsetBy(dx: 0.0, dy: heightDelta)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1998,9 +2015,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
previousApparentFrames.append((itemNode, itemNode.apparentFrame))
|
||||
}
|
||||
|
||||
if self.debugInfo {
|
||||
//print("replay before \(self.itemNodes.map({"\($0.index) \(unsafeAddressOf($0))"}))")
|
||||
}
|
||||
//var takenPreviousNodes = Set<ListViewItemNode>()
|
||||
|
||||
for operation in operations {
|
||||
switch operation {
|
||||
@ -2014,7 +2029,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
self.insertNodeAtIndex(animated: animated, animateAlpha: animateAlpha, previousFrame: previousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp)
|
||||
self.addSubnode(node)
|
||||
case let .InsertPlaceholder(index, referenceNode, offsetDirection):
|
||||
case let .InsertDisappearingPlaceholder(index, referenceNode, offsetDirection):
|
||||
var height: CGFloat?
|
||||
|
||||
for (node, previousFrame) in previousApparentFrames {
|
||||
@ -2666,143 +2681,6 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
return ListViewDisplayedItemRange(loadedRange: loadedRange, visibleRange: visibleRange)
|
||||
}
|
||||
|
||||
public func updateSizeAndInsets(size: CGSize, insets: UIEdgeInsets, duration: Double = 0.0, options: UIViewAnimationOptions = UIViewAnimationOptions()) {
|
||||
self.transactionQueue.addTransaction({ [weak self] completion in
|
||||
if let strongSelf = self {
|
||||
strongSelf.transactionOffset = 0.0
|
||||
strongSelf.updateSizeAndInsetsTransaction(size: size, insets: insets, duration: duration, options: options, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.transactionOffset = 0.0
|
||||
strongSelf.updateVisibleItemsTransaction(completion: completion)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
if useDynamicTuning {
|
||||
self.frictionSlider.frame = CGRect(x: 10.0, y: size.height - insets.bottom - 10.0 - self.frictionSlider.bounds.height, width: size.width - 20.0, height: self.frictionSlider.bounds.height)
|
||||
self.springSlider.frame = CGRect(x: 10.0, y: self.frictionSlider.frame.minY - self.springSlider.bounds.height, width: size.width - 20.0, height: self.springSlider.bounds.height)
|
||||
self.freeResistanceSlider.frame = CGRect(x: 10.0, y: self.springSlider.frame.minY - self.freeResistanceSlider.bounds.height, width: size.width - 20.0, height: self.freeResistanceSlider.bounds.height)
|
||||
self.scrollingResistanceSlider.frame = CGRect(x: 10.0, y: self.freeResistanceSlider.frame.minY - self.scrollingResistanceSlider.bounds.height, width: size.width - 20.0, height: self.scrollingResistanceSlider.bounds.height)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSizeAndInsetsTransaction(size: CGSize, insets: UIEdgeInsets, duration: Double, options: UIViewAnimationOptions, completion: @escaping (Void) -> Void) {
|
||||
if size.equalTo(self.visibleSize) && UIEdgeInsetsEqualToEdgeInsets(self.insets, insets) {
|
||||
completion()
|
||||
} else {
|
||||
if abs(size.width - self.visibleSize.width) > CGFloat(FLT_EPSILON) {
|
||||
let itemNodes = self.itemNodes
|
||||
for itemNode in itemNodes {
|
||||
itemNode.removeAllAnimations()
|
||||
itemNode.transitionOffset = 0.0
|
||||
if let index = itemNode.index {
|
||||
itemNode.layoutForWidth(size.width, item: self.items[index], previousItem: index == 0 ? nil : self.items[index - 1], nextItem: index == self.items.count - 1 ? nil : self.items[index + 1])
|
||||
}
|
||||
itemNode.apparentHeight = itemNode.bounds.height
|
||||
}
|
||||
|
||||
if itemNodes.count != 0 {
|
||||
for i in 0 ..< itemNodes.count - 1 {
|
||||
var nextFrame = itemNodes[i + 1].frame
|
||||
nextFrame.origin.y = itemNodes[i].apparentFrame.maxY
|
||||
itemNodes[i + 1].frame = nextFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var offsetFix = insets.top - self.insets.top
|
||||
|
||||
self.visibleSize = size
|
||||
self.insets = insets
|
||||
|
||||
var completeOffset = offsetFix
|
||||
|
||||
for itemNode in self.itemNodes {
|
||||
let position = itemNode.position
|
||||
itemNode.position = CGPoint(x: position.x, y: position.y + offsetFix)
|
||||
}
|
||||
|
||||
let completeDeltaHeight = offsetFix
|
||||
offsetFix = 0.0
|
||||
|
||||
if Double(completeDeltaHeight) < DBL_EPSILON && self.itemNodes.count != 0 {
|
||||
let firstItemNode = self.itemNodes[0]
|
||||
let lastItemNode = self.itemNodes[self.itemNodes.count - 1]
|
||||
|
||||
if lastItemNode.index == self.items.count - 1 {
|
||||
if firstItemNode.index == 0 {
|
||||
let topGap = firstItemNode.apparentFrame.origin.y - self.insets.top
|
||||
let bottomGap = self.visibleSize.height - lastItemNode.apparentFrame.maxY - self.insets.bottom
|
||||
if Double(bottomGap) > DBL_EPSILON {
|
||||
offsetFix = -bottomGap
|
||||
if topGap + bottomGap > 0.0 {
|
||||
offsetFix = topGap
|
||||
}
|
||||
|
||||
let absOffsetFix = abs(offsetFix)
|
||||
let absCompleteDeltaHeight = abs(completeDeltaHeight)
|
||||
offsetFix = min(absOffsetFix, absCompleteDeltaHeight) * (offsetFix < 0 ? -1.0 : 1.0)
|
||||
}
|
||||
} else {
|
||||
offsetFix = completeDeltaHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Double(abs(offsetFix)) > DBL_EPSILON {
|
||||
completeOffset -= offsetFix
|
||||
for itemNode in self.itemNodes {
|
||||
let position = itemNode.position
|
||||
itemNode.position = CGPoint(x: position.x, y: position.y - offsetFix)
|
||||
}
|
||||
}
|
||||
|
||||
self.snapToBounds()
|
||||
|
||||
self.ignoreScrollingEvents = true
|
||||
self.scroller.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.scroller.contentSize = CGSize(width: size.width, height: infiniteScrollSize * 2.0)
|
||||
self.lastContentOffset = CGPoint(x: 0.0, y: infiniteScrollSize)
|
||||
self.scroller.contentOffset = self.lastContentOffset
|
||||
|
||||
self.updateScroller()
|
||||
self.updateVisibleItemRange()
|
||||
|
||||
let completion = { [weak self] (_: Bool) -> Void in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateVisibleItemsTransaction(completion: completion)
|
||||
strongSelf.ignoreScrollingEvents = false
|
||||
}
|
||||
}
|
||||
|
||||
if duration > DBL_EPSILON {
|
||||
let animation: CABasicAnimation
|
||||
if (options.rawValue & UInt(7 << 16)) != 0 {
|
||||
let springAnimation = makeSpringAnimation("sublayerTransform")
|
||||
springAnimation.duration = duration * UIView.animationDurationFactor()
|
||||
springAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -completeOffset, 0.0))
|
||||
springAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity)
|
||||
springAnimation.isRemovedOnCompletion = true
|
||||
animation = springAnimation
|
||||
} else {
|
||||
let basicAnimation = CABasicAnimation(keyPath: "sublayerTransform")
|
||||
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
|
||||
basicAnimation.duration = duration * UIView.animationDurationFactor()
|
||||
basicAnimation.fromValue = NSValue(caTransform3D: CATransform3DMakeTranslation(0.0, -completeOffset, 0.0))
|
||||
basicAnimation.toValue = NSValue(caTransform3D: CATransform3DIdentity)
|
||||
basicAnimation.isRemovedOnCompletion = true
|
||||
animation = basicAnimation
|
||||
}
|
||||
|
||||
animation.completion = completion
|
||||
self.layer.add(animation, forKey: "sublayerTransform")
|
||||
} else {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateAnimations() {
|
||||
self.inVSync = true
|
||||
let actionsForVSync = self.actionsForVSync
|
||||
@ -2893,10 +2771,9 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
self.isTracking = true
|
||||
self.touchesPosition = (touches.first!).location(in: self.view)
|
||||
self.selectionTouchLocation = self.touchesPosition
|
||||
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
self.touchesPosition = touches.first!.location(in: self.view)
|
||||
self.selectionTouchLocation = touches.first!.location(in: self.view)
|
||||
|
||||
self.selectionTouchDelayTimer?.invalidate()
|
||||
let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in
|
||||
@ -2948,7 +2825,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func forEachItemNode(_ f: @noescape(ListViewItemNode) -> Void) {
|
||||
public func forEachItemNode(_ f: @noescape(ASDisplayNode) -> Void) {
|
||||
for itemNode in self.itemNodes {
|
||||
if itemNode.index != nil {
|
||||
f(itemNode)
|
||||
@ -2956,10 +2833,10 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
self.touchesPosition = touches.first!.location(in: self.view)
|
||||
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
if let selectionTouchLocation = self.selectionTouchLocation {
|
||||
let distance = CGPoint(x: selectionTouchLocation.x - self.touchesPosition.x, y: selectionTouchLocation.y - self.touchesPosition.y)
|
||||
let location = touches.first!.location(in: self.view)
|
||||
let distance = CGPoint(x: selectionTouchLocation.x - location.x, y: selectionTouchLocation.y - location.y)
|
||||
let maxMovementDistance: CGFloat = 4.0
|
||||
if distance.x * distance.x + distance.y * distance.y > maxMovementDistance * maxMovementDistance {
|
||||
self.selectionTouchLocation = nil
|
||||
@ -2972,9 +2849,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
super.touchesMoved(touches, with: event)
|
||||
}
|
||||
|
||||
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
self.isTracking = false
|
||||
|
||||
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
if let selectionTouchLocation = self.selectionTouchLocation {
|
||||
let index = self.itemIndexAtPoint(selectionTouchLocation)
|
||||
if index != self.highlightedItemIndex {
|
||||
@ -2998,16 +2873,14 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
|
||||
if let highlightedItemIndex = self.highlightedItemIndex {
|
||||
self.items[highlightedItemIndex].selected()
|
||||
self.items[highlightedItemIndex].selected(listView: self)
|
||||
}
|
||||
self.selectionTouchLocation = nil
|
||||
|
||||
super.touchesEnded(touches, with: event)
|
||||
}
|
||||
|
||||
override public func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
|
||||
self.isTracking = false
|
||||
|
||||
override open func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
|
||||
self.selectionTouchLocation = nil
|
||||
self.selectionTouchDelayTimer?.invalidate()
|
||||
self.selectionTouchDelayTimer = nil
|
||||
@ -3015,4 +2888,22 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
super.touchesCancelled(touches, with: event)
|
||||
}
|
||||
|
||||
@objc func trackingGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
self.isTracking = true
|
||||
break
|
||||
case .changed:
|
||||
self.touchesPosition = recognizer.location(in: self.view)
|
||||
case .ended, .cancelled:
|
||||
self.isTracking = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ public protocol ListViewItem {
|
||||
var floatingAccessoryItem: ListViewAccessoryItem? { get }
|
||||
var selectable: Bool { get }
|
||||
|
||||
func selected()
|
||||
func selected(listView: ListView)
|
||||
}
|
||||
|
||||
public extension ListViewItem {
|
||||
@ -35,7 +35,7 @@ public extension ListViewItem {
|
||||
return false
|
||||
}
|
||||
|
||||
func selected() {
|
||||
func selected(listView: ListView) {
|
||||
}
|
||||
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {
|
||||
|
@ -2,16 +2,16 @@ import Foundation
|
||||
import AsyncDisplayKit
|
||||
|
||||
var testSpringFrictionLimits: (CGFloat, CGFloat) = (3.0, 60.0)
|
||||
var testSpringFriction: CGFloat = 33.26
|
||||
var testSpringFriction: CGFloat = 29.3323
|
||||
|
||||
var testSpringConstantLimits: (CGFloat, CGFloat) = (3.0, 450.0)
|
||||
var testSpringConstant: CGFloat = 450.0
|
||||
var testSpringConstant: CGFloat = 353.6746
|
||||
|
||||
var testSpringResistanceFreeLimits: (CGFloat, CGFloat) = (0.05, 1.0)
|
||||
var testSpringFreeResistance: CGFloat = 1.0
|
||||
var testSpringFreeResistance: CGFloat = 0.6721
|
||||
|
||||
var testSpringResistanceScrollingLimits: (CGFloat, CGFloat) = (0.1, 1.0)
|
||||
var testSpringScrollingResistance: CGFloat = 0.4
|
||||
var testSpringScrollingResistance: CGFloat = 0.6721
|
||||
|
||||
struct ListViewItemSpring {
|
||||
let stiffness: CGFloat
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
import SwiftSignalKit
|
||||
|
||||
public typealias ListViewTransaction = @escaping (@escaping (Void) -> Void) -> Void
|
||||
public typealias ListViewTransaction = (@escaping (Void) -> Void) -> Void
|
||||
|
||||
public final class ListViewTransactionQueue {
|
||||
private var transactions: [ListViewTransaction] = []
|
||||
@ -10,7 +10,7 @@ public final class ListViewTransactionQueue {
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func addTransaction(_ transaction: ListViewTransaction) {
|
||||
public func addTransaction(_ transaction: @escaping ListViewTransaction) {
|
||||
let beginTransaction = self.transactions.count == 0
|
||||
self.transactions.append(transaction)
|
||||
|
||||
|
@ -176,7 +176,7 @@ public class NavigationBar: ASDisplayNode {
|
||||
}
|
||||
self._previousItem = value
|
||||
|
||||
if let previousItem = value {
|
||||
if let previousItem = value, previousItem.backBarButtonItem == nil {
|
||||
self.previousItemListenerKey = previousItem.addSetTitleListener { [weak self] text in
|
||||
if let strongSelf = self {
|
||||
strongSelf.backButtonNode.text = text ?? "Back"
|
||||
@ -204,7 +204,11 @@ public class NavigationBar: ASDisplayNode {
|
||||
self.leftButtonNode.removeFromSupernode()
|
||||
|
||||
if let previousItem = self.previousItem {
|
||||
self.backButtonNode.text = previousItem.title ?? "Back"
|
||||
if let backBarButtonItem = previousItem.backBarButtonItem {
|
||||
self.backButtonNode.text = backBarButtonItem.title ?? "Back"
|
||||
} else {
|
||||
self.backButtonNode.text = previousItem.title ?? "Back"
|
||||
}
|
||||
|
||||
if self.backButtonNode.supernode == nil {
|
||||
self.clippingNode.addSubnode(self.backButtonNode)
|
||||
|
@ -55,11 +55,11 @@ final class SystemContainedControllerTransitionCoordinator: NSObject, UIViewCont
|
||||
return CGAffineTransform.identity
|
||||
}
|
||||
|
||||
public func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)?, completion: (@escaping (UIViewControllerTransitionCoordinatorContext) -> Swift.Void)? = nil) -> Bool {
|
||||
public func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)? = nil) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public func animateAlongsideTransition(in view: UIView?, animation: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)?, completion: (@escaping (UIViewControllerTransitionCoordinatorContext) -> Swift.Void)? = nil) -> Bool {
|
||||
public func animateAlongsideTransition(in view: UIView?, animation: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Swift.Void)? = nil) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -8,5 +8,6 @@
|
||||
@end
|
||||
|
||||
CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath);
|
||||
CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPath, CGFloat initialVelocity);
|
||||
CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t);
|
||||
|
||||
|
@ -42,6 +42,17 @@ CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath) {
|
||||
return springAnimation;
|
||||
}
|
||||
|
||||
CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPath, CGFloat initialVelocity) {
|
||||
CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath];
|
||||
springAnimation.mass = 5.0f;
|
||||
springAnimation.stiffness = 900.0f;
|
||||
springAnimation.damping = 88.0f;
|
||||
springAnimation.initialVelocity = initialVelocity;
|
||||
springAnimation.duration = springAnimation.settlingDuration;
|
||||
springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
|
||||
return springAnimation;
|
||||
}
|
||||
|
||||
CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t) {
|
||||
return [(CASpringAnimation *)animation _solveForInput:t];
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user