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 */; };
|
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 */,
|
||||||
|
@ -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()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
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 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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];
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user