no message

This commit is contained in:
Peter 2016-10-07 19:13:32 +03:00
parent 684ab193c8
commit a3bbfd59b3
15 changed files with 627 additions and 209 deletions

View File

@ -13,6 +13,10 @@
D015F7541D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7531D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift */; }; D015F7541D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7531D1B0F6C00E269B5 /* SystemContainedControllerTransitionCoordinator.swift */; };
D015F7581D1B467200E269B5 /* ActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7571D1B467200E269B5 /* ActionSheetController.swift */; }; D015F7581D1B467200E269B5 /* ActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7571D1B467200E269B5 /* ActionSheetController.swift */; };
D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D015F7591D1B46B600E269B5 /* ActionSheetControllerNode.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 */; }; D02958001D6F096000360E5E /* ContextMenuContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02957FF1D6F096000360E5E /* ContextMenuContainerNode.swift */; };
D02BDB021B6AC703008AFAD2 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */; }; D02BDB021B6AC703008AFAD2 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BDB011B6AC703008AFAD2 /* RuntimeUtils.swift */; };
D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03725C01D6DF594007FC290 /* ContextMenuNode.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D03725C01D6DF594007FC290 /* ContextMenuNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenuNode.swift; sourceTree = "<group>"; };
@ -269,6 +277,26 @@
name = "Action Sheet"; name = "Action Sheet";
sourceTree = "<group>"; 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 */ = { D02BDAEC1B6A7053008AFAD2 /* Nodes */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -340,7 +368,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D08122991D19A9E0005F7395 /* User Interface */, D08122991D19A9E0005F7395 /* User Interface */,
D0C2DFBA1CC443080044FF83 /* List View */, D01E2BE51D904A530066BF65 /* Collection Nodes */,
D03BCCE91C72AE4B0097A291 /* Theme */, D03BCCE91C72AE4B0097A291 /* Theme */,
D05CC3001B6955D500E235A3 /* Utils */, D05CC3001B6955D500E235A3 /* Utils */,
D07921AA1B6FC911005C23D9 /* Status Bar */, D07921AA1B6FC911005C23D9 /* Status Bar */,
@ -483,7 +511,7 @@
name = Controllers; name = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D0C2DFBA1CC443080044FF83 /* List View */ = { D0C2DFBA1CC443080044FF83 /* List Node */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D0C2DFBB1CC4431D0044FF83 /* ASTransformLayerNode.swift */, D0C2DFBB1CC4431D0044FF83 /* ASTransformLayerNode.swift */,
@ -497,7 +525,7 @@
D0C2DFC41CC4431D0044FF83 /* ListViewScroller.swift */, D0C2DFC41CC4431D0044FF83 /* ListViewScroller.swift */,
D0C2DFC51CC4431D0044FF83 /* ListViewAccessoryItemNode.swift */, D0C2DFC51CC4431D0044FF83 /* ListViewAccessoryItemNode.swift */,
); );
name = "List View"; name = "List Node";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D0DC48521BF93D7C00F672FD /* Tabs */ = { D0DC48521BF93D7C00F672FD /* Tabs */ = {
@ -661,6 +689,8 @@
D03E7DFF1C96F7B400C07816 /* StatusBarManager.swift in Sources */, D03E7DFF1C96F7B400C07816 /* StatusBarManager.swift in Sources */,
D05CC3161B695A9600E235A3 /* NavigationBar.swift in Sources */, D05CC3161B695A9600E235A3 /* NavigationBar.swift in Sources */,
D05CC31D1B695A9600E235A3 /* UIBarButtonItem+Proxy.m in Sources */, D05CC31D1B695A9600E235A3 /* UIBarButtonItem+Proxy.m in Sources */,
D01E2BDE1D9049620066BF65 /* GridNode.swift in Sources */,
D01E2BE01D90498E0066BF65 /* GridNodeScroller.swift in Sources */,
D0C85DD61D1C600D00124894 /* ActionSheetButtonNode.swift in Sources */, D0C85DD61D1C600D00124894 /* ActionSheetButtonNode.swift in Sources */,
D0C2DFD01CC4431D0044FF83 /* ListViewAccessoryItemNode.swift in Sources */, D0C2DFD01CC4431D0044FF83 /* ListViewAccessoryItemNode.swift in Sources */,
D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */, D0D94A171D3814F900740E02 /* UniversalTapRecognizer.swift in Sources */,
@ -691,8 +721,10 @@
D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */, D015F75A1D1B46B600E269B5 /* ActionSheetControllerNode.swift in Sources */,
D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */, D03725C11D6DF594007FC290 /* ContextMenuNode.swift in Sources */,
D053CB611D22B4F200DD41DF /* CATracingLayer.m in Sources */, D053CB611D22B4F200DD41DF /* CATracingLayer.m in Sources */,
D01E2BE41D904A000066BF65 /* GridItem.swift in Sources */,
D081229D1D19AA1C005F7395 /* ContainerViewLayout.swift in Sources */, D081229D1D19AA1C005F7395 /* ContainerViewLayout.swift in Sources */,
D0C2DFC71CC4431D0044FF83 /* ListViewItemNode.swift in Sources */, D0C2DFC71CC4431D0044FF83 /* ListViewItemNode.swift in Sources */,
D01E2BE21D9049F60066BF65 /* GridItemNode.swift in Sources */,
D08E903A1D24159200533158 /* ActionSheetItem.swift in Sources */, D08E903A1D24159200533158 /* ActionSheetItem.swift in Sources */,
D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */, D0AE2CA61C94548900F2FD3C /* GenerateImage.swift in Sources */,
D05CC2EC1B69558A00E235A3 /* RuntimeUtils.m in Sources */, D05CC2EC1B69558A00E235A3 /* RuntimeUtils.m in Sources */,

View File

@ -83,6 +83,28 @@ public extension CALayer {
self.add(animation, forKey: keyPath) 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) { 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()) 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) 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 from == to {
if let completion = completion { if let completion = completion {
completion(true) completion(true)
@ -146,7 +168,29 @@ public extension CALayer {
} }
return 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) var interrupted = false
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 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()
})
} }
} }

View File

@ -10,6 +10,7 @@ final class ContextMenuNode: ASDisplayNode {
private let actionNodes: [ContextMenuActionNode] private let actionNodes: [ContextMenuActionNode]
var sourceRect: CGRect? var sourceRect: CGRect?
var arrowOnBottom: Bool = true
private var dismissedByTouchOutside = false private var dismissedByTouchOutside = false
@ -59,6 +60,7 @@ final class ContextMenuNode: ASDisplayNode {
verticalOrigin = min(layout.size.height - insets.bottom - 54.0, sourceRect.maxY) verticalOrigin = min(layout.size.height - insets.bottom - 54.0, sourceRect.maxY)
arrowOnBottom = false arrowOnBottom = false
} }
self.arrowOnBottom = arrowOnBottom
let horizontalOrigin: CGFloat = floor(min(max(8.0, sourceRect.midX - actionsWidth / 2.0), layout.size.width - actionsWidth - 8.0)) 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() { 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) { func animateOut(completion: @escaping () -> Void) {

5
Display/GridItem.swift Normal file
View File

@ -0,0 +1,5 @@
import Foundation
public protocol GridItem {
func node(layout: GridNodeLayout) -> GridItemNode
}

View File

@ -0,0 +1,6 @@
import Foundation
import AsyncDisplayKit
open class GridItemNode: ASDisplayNode {
}

388
Display/GridNode.swift Normal file
View 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)
}
}
}

View 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")
}
}

View File

@ -99,11 +99,13 @@ public struct ListViewInsertItem {
public struct ListViewUpdateItem { public struct ListViewUpdateItem {
public let index: Int public let index: Int
public let previousIndex: Int
public let item: ListViewItem public let item: ListViewItem
public let directionHint: ListViewItemOperationDirectionHint? 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.index = index
self.previousIndex = previousIndex
self.item = item self.item = item
self.directionHint = directionHint self.directionHint = directionHint
} }
@ -579,8 +581,9 @@ private struct ListViewState {
while i >= 0 { while i >= 0 {
let itemNode = self.nodes[i] let itemNode = self.nodes[i]
let frame = itemNode.frame let frame = itemNode.frame
//print("node \(i) frame \(frame)")
if frame.maxY < -self.invisibleInset || frame.origin.y > self.visibleSize.height + self.invisibleInset { 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)) operations.append(.Remove(index: i, offsetDirection: frame.maxY < -self.invisibleInset ? .Down : .Up))
self.nodes.remove(at: i) self.nodes.remove(at: i)
} }
@ -599,7 +602,7 @@ private struct ListViewState {
if self.nodes[j].frame.maxY < upperBound { if self.nodes[j].frame.maxY < upperBound {
if let index = self.nodes[j].index { if let index = self.nodes[j].index {
if index != previousIndex - 1 { if index != previousIndex - 1 {
print("remove monotonity \(j) (\(index))") //print("remove monotonity \(j) (\(index))")
operations.append(.Remove(index: j, offsetDirection: .Down)) operations.append(.Remove(index: j, offsetDirection: .Down))
self.nodes.remove(at: j) self.nodes.remove(at: j)
} else { } else {
@ -633,7 +636,7 @@ private struct ListViewState {
} }
if !removeIndices.isEmpty { if !removeIndices.isEmpty {
for i in removeIndices.reversed() { 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)) operations.append(.Remove(index: i, offsetDirection: .Up))
self.nodes.remove(at: i) self.nodes.remove(at: i)
} }
@ -776,7 +779,7 @@ private struct ListViewState {
if let referenceNode = referenceNode , animated { if let referenceNode = referenceNode , animated {
self.nodes.insert(.Placeholder(frame: nodeFrame), at: index) 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 { } else {
if nodeFrame.maxY > self.insets.top - CGFloat(FLT_EPSILON) { 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) { 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: case .System:
operations.append(.UpdateLayout(index: itemIndex, layout: layout, apply: apply)) operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
} }
break break
@ -862,7 +865,7 @@ private struct ListViewState {
private enum ListViewStateOperation { private enum ListViewStateOperation {
case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, node: ListViewItemNode, layout: ListViewItemNodeLayout, apply: () -> ()) 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 Remove(index: Int, offsetDirection: ListViewInsertionOffsetDirection)
case Remap([Int: Int]) case Remap([Int: Int])
case UpdateLayout(index: Int, layout: ListViewItemNodeLayout, apply: () -> ()) case UpdateLayout(index: Int, layout: ListViewItemNodeLayout, apply: () -> ())
@ -928,7 +931,7 @@ public enum ListViewVisibleContentOffset {
case none case none
} }
public final class ListView: ASDisplayNode, UIScrollViewDelegate { open class ListView: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
private final let scroller: ListViewScroller private final let scroller: ListViewScroller
private final var visibleSize: CGSize = CGSize() private final var visibleSize: CGSize = CGSize()
private final var insets = UIEdgeInsets() private final var insets = UIEdgeInsets()
@ -1038,6 +1041,10 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
self.scroller.panGestureRecognizer.cancelsTouchesInView = true self.scroller.panGestureRecognizer.cancelsTouchesInView = true
self.view.addGestureRecognizer(self.scroller.panGestureRecognizer) 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 = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes) self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
if #available(iOS 10.0, *) { if #available(iOS 10.0, *) {
@ -1181,17 +1188,18 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
var useScrollDynamics = false 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 { for itemNode in self.itemNodes {
if itemNode.wantsScrollDynamics { if itemNode.wantsScrollDynamics {
useScrollDynamics = true 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 var distance: CGFloat
let itemFrame = itemNode.apparentFrame 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) { 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) { if let updateSizeAndInsets = updateSizeAndInsets , self.items.count == 0 || (updateSizeAndInsets.size == self.visibleSize && updateSizeAndInsets.insets == self.insets) {
self.visibleSize = updateSizeAndInsets.size self.visibleSize = updateSizeAndInsets.size
self.insets = updateSizeAndInsets.insets 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.ignoreScrollingEvents = true
self.scroller.frame = CGRect(origin: CGPoint(), size: updateSizeAndInsets.size) self.scroller.frame = CGRect(origin: CGPoint(), size: updateSizeAndInsets.size)
self.scroller.contentSize = CGSize(width: updateSizeAndInsets.size.width, height: infiniteScrollSize * 2.0) 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] = [:] var previousNodes: [Int: ListViewItemNode] = [:]
for insertedItem in sortedIndicesAndItems { for insertedItem in sortedIndicesAndItems {
self.items.insert(insertedItem.item, at: insertedItem.index) self.items.insert(insertedItem.item, at: insertedItem.index)
@ -1517,8 +1522,9 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
for updatedItem in updateIndicesAndItems { for updatedItem in updateIndicesAndItems {
self.items[updatedItem.index] = updatedItem.item self.items[updatedItem.index] = updatedItem.item
for itemNode in self.itemNodes { for itemNode in self.itemNodes {
if itemNode.index == updatedItem.index { if itemNode.index == updatedItem.previousIndex {
previousNodes[updatedItem.index] = itemNode previousNodes[updatedItem.index] = itemNode
break
} }
} }
} }
@ -1750,12 +1756,23 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
var updatedState = state var updatedState = state
var updatedOperations = operations var updatedOperations = operations
let heightDelta = layout.size.height - updatedState.nodes[i].frame.size.height
updatedOperations.append(.UpdateLayout(index: i, layout: layout, apply: apply)) updatedOperations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
if nodeIndex + 1 < updatedState.nodes.count { if !animated {
for i in nodeIndex + 1 ..< updatedState.nodes.count { let previousFrame = updatedState.nodes[i].frame
let frame = updatedState.nodes[i].frame updatedState.nodes[i].frame = CGRect(origin: previousFrame.origin, size: layout.size)
updatedState.nodes[i].frame = frame.offsetBy(dx: 0.0, dy: frame.size.height) 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)) previousApparentFrames.append((itemNode, itemNode.apparentFrame))
} }
if self.debugInfo { //var takenPreviousNodes = Set<ListViewItemNode>()
//print("replay before \(self.itemNodes.map({"\($0.index) \(unsafeAddressOf($0))"}))")
}
for operation in operations { for operation in operations {
switch operation { 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.insertNodeAtIndex(animated: animated, animateAlpha: animateAlpha, previousFrame: previousFrame, nodeIndex: index, offsetDirection: offsetDirection, node: node, layout: layout, apply: apply, timestamp: timestamp)
self.addSubnode(node) self.addSubnode(node)
case let .InsertPlaceholder(index, referenceNode, offsetDirection): case let .InsertDisappearingPlaceholder(index, referenceNode, offsetDirection):
var height: CGFloat? var height: CGFloat?
for (node, previousFrame) in previousApparentFrames { for (node, previousFrame) in previousApparentFrames {
@ -2666,143 +2681,6 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
return ListViewDisplayedItemRange(loadedRange: loadedRange, visibleRange: visibleRange) 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() { private func updateAnimations() {
self.inVSync = true self.inVSync = true
let actionsForVSync = self.actionsForVSync let actionsForVSync = self.actionsForVSync
@ -2893,10 +2771,9 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
} }
} }
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.isTracking = true self.touchesPosition = touches.first!.location(in: self.view)
self.touchesPosition = (touches.first!).location(in: self.view) self.selectionTouchLocation = touches.first!.location(in: self.view)
self.selectionTouchLocation = self.touchesPosition
self.selectionTouchDelayTimer?.invalidate() self.selectionTouchDelayTimer?.invalidate()
let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in let timer = Timer(timeInterval: 0.08, target: ListViewTimerProxy { [weak self] in
@ -2948,7 +2825,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
return nil return nil
} }
public func forEachItemNode(_ f: @noescape(ListViewItemNode) -> Void) { public func forEachItemNode(_ f: @noescape(ASDisplayNode) -> Void) {
for itemNode in self.itemNodes { for itemNode in self.itemNodes {
if itemNode.index != nil { if itemNode.index != nil {
f(itemNode) f(itemNode)
@ -2956,10 +2833,10 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
} }
} }
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
self.touchesPosition = touches.first!.location(in: self.view)
if let selectionTouchLocation = self.selectionTouchLocation { 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 let maxMovementDistance: CGFloat = 4.0
if distance.x * distance.x + distance.y * distance.y > maxMovementDistance * maxMovementDistance { if distance.x * distance.x + distance.y * distance.y > maxMovementDistance * maxMovementDistance {
self.selectionTouchLocation = nil self.selectionTouchLocation = nil
@ -2972,9 +2849,7 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
super.touchesMoved(touches, with: event) super.touchesMoved(touches, with: event)
} }
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.isTracking = false
if let selectionTouchLocation = self.selectionTouchLocation { if let selectionTouchLocation = self.selectionTouchLocation {
let index = self.itemIndexAtPoint(selectionTouchLocation) let index = self.itemIndexAtPoint(selectionTouchLocation)
if index != self.highlightedItemIndex { if index != self.highlightedItemIndex {
@ -2998,16 +2873,14 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
} }
if let highlightedItemIndex = self.highlightedItemIndex { if let highlightedItemIndex = self.highlightedItemIndex {
self.items[highlightedItemIndex].selected() self.items[highlightedItemIndex].selected(listView: self)
} }
self.selectionTouchLocation = nil self.selectionTouchLocation = nil
super.touchesEnded(touches, with: event) super.touchesEnded(touches, with: event)
} }
override public func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) { override open func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
self.isTracking = false
self.selectionTouchLocation = nil self.selectionTouchLocation = nil
self.selectionTouchDelayTimer?.invalidate() self.selectionTouchDelayTimer?.invalidate()
self.selectionTouchDelayTimer = nil self.selectionTouchDelayTimer = nil
@ -3015,4 +2888,22 @@ public final class ListView: ASDisplayNode, UIScrollViewDelegate {
super.touchesCancelled(touches, with: event) 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
}
} }

View File

@ -15,7 +15,7 @@ public protocol ListViewItem {
var floatingAccessoryItem: ListViewAccessoryItem? { get } var floatingAccessoryItem: ListViewAccessoryItem? { get }
var selectable: Bool { get } var selectable: Bool { get }
func selected() func selected(listView: ListView)
} }
public extension ListViewItem { public extension ListViewItem {
@ -35,7 +35,7 @@ public extension ListViewItem {
return false 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) { func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) {

View File

@ -2,16 +2,16 @@ import Foundation
import AsyncDisplayKit import AsyncDisplayKit
var testSpringFrictionLimits: (CGFloat, CGFloat) = (3.0, 60.0) 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 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 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 testSpringResistanceScrollingLimits: (CGFloat, CGFloat) = (0.1, 1.0)
var testSpringScrollingResistance: CGFloat = 0.4 var testSpringScrollingResistance: CGFloat = 0.6721
struct ListViewItemSpring { struct ListViewItemSpring {
let stiffness: CGFloat let stiffness: CGFloat

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
import SwiftSignalKit import SwiftSignalKit
public typealias ListViewTransaction = @escaping (@escaping (Void) -> Void) -> Void public typealias ListViewTransaction = (@escaping (Void) -> Void) -> Void
public final class ListViewTransactionQueue { public final class ListViewTransactionQueue {
private var transactions: [ListViewTransaction] = [] private var transactions: [ListViewTransaction] = []
@ -10,7 +10,7 @@ public final class ListViewTransactionQueue {
public init() { public init() {
} }
public func addTransaction(_ transaction: ListViewTransaction) { public func addTransaction(_ transaction: @escaping ListViewTransaction) {
let beginTransaction = self.transactions.count == 0 let beginTransaction = self.transactions.count == 0
self.transactions.append(transaction) self.transactions.append(transaction)

View File

@ -176,7 +176,7 @@ public class NavigationBar: ASDisplayNode {
} }
self._previousItem = value self._previousItem = value
if let previousItem = value { if let previousItem = value, previousItem.backBarButtonItem == nil {
self.previousItemListenerKey = previousItem.addSetTitleListener { [weak self] text in self.previousItemListenerKey = previousItem.addSetTitleListener { [weak self] text in
if let strongSelf = self { if let strongSelf = self {
strongSelf.backButtonNode.text = text ?? "Back" strongSelf.backButtonNode.text = text ?? "Back"
@ -204,7 +204,11 @@ public class NavigationBar: ASDisplayNode {
self.leftButtonNode.removeFromSupernode() self.leftButtonNode.removeFromSupernode()
if let previousItem = self.previousItem { 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 { if self.backButtonNode.supernode == nil {
self.clippingNode.addSubnode(self.backButtonNode) self.clippingNode.addSubnode(self.backButtonNode)

View File

@ -55,11 +55,11 @@ final class SystemContainedControllerTransitionCoordinator: NSObject, UIViewCont
return CGAffineTransform.identity 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 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 return false
} }

View File

@ -8,5 +8,6 @@
@end @end
CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath); CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath);
CABasicAnimation * _Nonnull makeSpringBounceAnimation(NSString * _Nonnull keyPath, CGFloat initialVelocity);
CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t); CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t);

View File

@ -42,6 +42,17 @@ CABasicAnimation * _Nonnull makeSpringAnimation(NSString * _Nonnull keyPath) {
return springAnimation; 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) { CGFloat springAnimationValueAt(CABasicAnimation * _Nonnull animation, CGFloat t) {
return [(CASpringAnimation *)animation _solveForInput:t]; return [(CASpringAnimation *)animation _solveForInput:t];
} }