Added CollectionIndexNode

This commit is contained in:
Peter Iakovlev 2018-12-11 17:51:48 +04:00
parent c8a8570465
commit d71707c31b
6 changed files with 191 additions and 13 deletions

View File

@ -76,6 +76,7 @@
D03E7DF91C96C5F200C07816 /* NSWeakReference.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E7DF71C96C5F200C07816 /* NSWeakReference.m */; };
D03E7DFF1C96F7B400C07816 /* StatusBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E7DFE1C96F7B400C07816 /* StatusBarManager.swift */; };
D03E7E011C974AB300C07816 /* DisplayLinkDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E7E001C974AB300C07816 /* DisplayLinkDispatcher.swift */; };
D04554AA21BDB93E007A6DD9 /* CollectionIndexNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04554A921BDB93E007A6DD9 /* CollectionIndexNode.swift */; };
D04C468E1F4C97BE00D30FE1 /* PageControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04C468D1F4C97BE00D30FE1 /* PageControlNode.swift */; };
D05174B31EAA833200A1BF36 /* CASeeThroughTracingLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D05174B11EAA833200A1BF36 /* CASeeThroughTracingLayer.h */; settings = {ATTRIBUTES = (Public, ); }; };
D05174B41EAA833200A1BF36 /* CASeeThroughTracingLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D05174B21EAA833200A1BF36 /* CASeeThroughTracingLayer.m */; };
@ -254,6 +255,7 @@
D03E7DF71C96C5F200C07816 /* NSWeakReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSWeakReference.m; sourceTree = "<group>"; };
D03E7DFE1C96F7B400C07816 /* StatusBarManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarManager.swift; sourceTree = "<group>"; };
D03E7E001C974AB300C07816 /* DisplayLinkDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayLinkDispatcher.swift; sourceTree = "<group>"; };
D04554A921BDB93E007A6DD9 /* CollectionIndexNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionIndexNode.swift; sourceTree = "<group>"; };
D04C468D1F4C97BE00D30FE1 /* PageControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageControlNode.swift; sourceTree = "<group>"; };
D05174B11EAA833200A1BF36 /* CASeeThroughTracingLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CASeeThroughTracingLayer.h; sourceTree = "<group>"; };
D05174B21EAA833200A1BF36 /* CASeeThroughTracingLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CASeeThroughTracingLayer.m; sourceTree = "<group>"; };
@ -508,6 +510,7 @@
D0FA08C32048803C00DD23FC /* TextNode.swift */,
D0FA08C5204880C900DD23FC /* ImmediateTextNode.swift */,
D0CA3F892073F7650042D2B6 /* LinkHighlightingNode.swift */,
D04554A921BDB93E007A6DD9 /* CollectionIndexNode.swift */,
);
name = Nodes;
sourceTree = "<group>";
@ -1012,6 +1015,7 @@
D0CA3F8A2073F7650042D2B6 /* LinkHighlightingNode.swift in Sources */,
D0C2DFCB1CC4431D0044FF83 /* ListViewAnimation.swift in Sources */,
D0BE93191E8ED71100DCC1E6 /* NativeWindowHostView.swift in Sources */,
D04554AA21BDB93E007A6DD9 /* CollectionIndexNode.swift in Sources */,
D05CC3251B695B0700E235A3 /* NavigationBarProxy.m in Sources */,
D05174B41EAA833200A1BF36 /* CASeeThroughTracingLayer.m in Sources */,
D03AA4D9202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift in Sources */,

View File

@ -0,0 +1,141 @@
import Foundation
import AsyncDisplayKit
private let titleFont = Font.bold(11.0)
public final class CollectionIndexNode: ASDisplayNode {
public static let searchIndex: String = "_$search$_"
private var currentSize: CGSize?
private var currentSections: [String] = []
private var currentColor: UIColor?
private var titleNodes: [String: (node: ImmediateTextNode, size: CGSize)] = [:]
private var currentSelectedIndex: String?
public var indexSelected: ((String) -> Void)?
override public init() {
super.init()
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
}
public func update(size: CGSize, color: UIColor, sections: [String], transition: ContainedViewLayoutTransition) {
if self.currentColor == nil || !color.isEqual(self.currentColor) {
self.currentColor = color
for (title, nodeAndSize) in self.titleNodes {
nodeAndSize.node.attributedText = NSAttributedString(string: title, font: titleFont, textColor: color)
let _ = nodeAndSize.node.updateLayout(CGSize(width: 100.0, height: 100.0))
}
}
if self.currentSize == size && self.currentSections == sections {
return
}
self.currentSize = size
self.currentSections = sections
let itemHeight: CGFloat = 15.0
let verticalInset: CGFloat = 10.0
let maxHeight = size.height - verticalInset * 2.0
let maxItemCount = min(sections.count, Int(floor(maxHeight / itemHeight)))
let skipCount = Int(ceil(CGFloat(sections.count) / CGFloat(maxItemCount)))
let actualCount: CGFloat = ceil(CGFloat(sections.count) / CGFloat(skipCount))
let totalHeight = actualCount * itemHeight
let verticalOrigin = verticalInset + floor((maxHeight - totalHeight) / 2.0)
var validTitles = Set<String>()
var index = 0
var displayIndex = 0
while index < sections.count {
let title = sections[index]
let nodeAndSize: (node: ImmediateTextNode, size: CGSize)
var animate = false
if let current = self.titleNodes[title] {
animate = true
nodeAndSize = current
} else {
let node = ImmediateTextNode()
node.attributedText = NSAttributedString(string: title, font: titleFont, textColor: color)
let nodeSize = node.updateLayout(CGSize(width: 100.0, height: 100.0))
nodeAndSize = (node, nodeSize)
self.addSubnode(node)
self.titleNodes[title] = nodeAndSize
}
validTitles.insert(title)
let previousPosition = nodeAndSize.node.position
nodeAndSize.node.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeAndSize.size.width) / 2.0), y: verticalOrigin + itemHeight * CGFloat(displayIndex) + floor((itemHeight - nodeAndSize.size.height) / 2.0)), size: nodeAndSize.size)
if animate {
transition.animatePosition(node: nodeAndSize.node, from: previousPosition)
}
index += skipCount
displayIndex += 1
}
var removeTitles: [String] = []
for title in self.titleNodes.keys {
if !validTitles.contains(title) {
removeTitles.append(title)
}
}
for title in removeTitles {
self.titleNodes.removeValue(forKey: title)?.node.removeFromSupernode()
}
}
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.insetBy(dx: -5.0, dy: 0.0).contains(point) {
return self.view
} else {
return nil
}
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
var locationTitleAndPosition: (String, CGFloat)?
let location = recognizer.location(in: self.view)
for (title, nodeAndSize) in self.titleNodes {
let nodeFrame = nodeAndSize.node.frame
if location.y >= nodeFrame.minY - 5.0 && location.y <= nodeFrame.maxY + 5.0 {
if let currentTitleAndPosition = locationTitleAndPosition {
let distance = abs(nodeFrame.midY - location.y)
let previousDistance = abs(currentTitleAndPosition.1 - location.y)
if distance < previousDistance {
locationTitleAndPosition = (title, nodeFrame.midY)
}
} else {
locationTitleAndPosition = (title, nodeFrame.midY)
}
}
}
let locationTitle = locationTitleAndPosition?.0
switch recognizer.state {
case .began:
self.currentSelectedIndex = locationTitle
if let locationTitle = locationTitle {
self.indexSelected?(locationTitle)
}
case .changed:
if locationTitle != self.currentSelectedIndex {
self.currentSelectedIndex = locationTitle
if let locationTitle = locationTitle {
self.indexSelected?(locationTitle)
}
}
case .cancelled, .ended:
self.currentSelectedIndex = nil
default:
break
}
}
}

View File

@ -137,16 +137,16 @@ public extension ContainedViewLayoutTransition {
func animatePosition(node: ASDisplayNode, from position: CGPoint, completion: ((Bool) -> Void)? = nil) {
switch self {
case .immediate:
if let completion = completion {
completion(true)
}
case let .animated(duration, curve):
node.layer.animatePosition(from: position, to: node.position, duration: duration, timingFunction: curve.timingFunction, completion: { result in
case .immediate:
if let completion = completion {
completion(result)
completion(true)
}
})
case let .animated(duration, curve):
node.layer.animatePosition(from: position, to: node.position, duration: duration, timingFunction: curve.timingFunction, completion: { result in
if let completion = completion {
completion(result)
}
})
}
}

View File

@ -227,6 +227,12 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
}
}
public var indicatorStyle: UIScrollViewIndicatorStyle = .default {
didSet {
self.scrollView.indicatorStyle = self.indicatorStyle
}
}
public private(set) var opaqueState: Any?
public override init() {
@ -755,6 +761,9 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
private func applyPresentaionLayoutTransition(_ presentationLayoutTransition: GridNodePresentationLayoutTransition, removedNodes: [GridItemNode], updateLayoutTransition: ContainedViewLayoutTransition?, customScrollToItem: Bool, itemTransition: ContainedViewLayoutTransition, synchronousLoads: Bool, updatingLayout: Bool, completion: (GridNodeDisplayedItemRange) -> Void) {
let boundsTransition: ContainedViewLayoutTransition = updateLayoutTransition ?? .immediate
var addedNodes = false
let verticalIndicator = self.scrollView.subviews.last as? UIImageView
var previousItemFrames: [WrappedGridItemNode: CGRect]?
var saveItemFrames = false
switch presentationLayoutTransition.transition {
@ -782,7 +791,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
previousItemFrames = itemFrames
}
applyingContentOffset = true
self.applyingContentOffset = true
let previousBounds = self.bounds
self.scrollView.contentSize = presentationLayoutTransition.layout.contentSize
@ -819,6 +828,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
let itemNode = self.items[item.index].node(layout: presentationLayoutTransition.layout.layout, synchronousLoad: synchronousLoads)
itemNode.frame = item.frame
self.addItemNode(index: item.index, itemNode: itemNode, lowestSectionNode: lowestSectionNode)
addedNodes = true
itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads)
}
}
@ -845,6 +855,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
let sectionNode = section.section.node()
sectionNode.frame = sectionFrame
self.addSectionNode(section: wrappedSection, sectionNode: sectionNode)
addedNodes = true
}
}
@ -1100,6 +1111,12 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
}
}
if addedNodes {
if let verticalIndicator = verticalIndicator, self.scrollView.subviews.last !== verticalIndicator {
verticalIndicator.superview?.bringSubview(toFront: verticalIndicator)
}
}
if let presentationLayoutUpdated = self.presentationLayoutUpdated {
presentationLayoutUpdated(GridNodeCurrentPresentationLayout(layout: presentationLayoutTransition.layout.layout, contentOffset: presentationLayoutTransition.layout.contentOffset, contentSize: presentationLayoutTransition.layout.contentSize), updateLayoutTransition ?? presentationLayoutTransition.transition)
}

View File

@ -1,5 +1,10 @@
import Foundation
public struct ImmediateTextNodeLayoutInfo {
public let size: CGSize
public let truncated: Bool
}
public class ImmediateTextNode: TextNode {
public var attributedText: NSAttributedString?
public var textAlignment: NSTextAlignment = .natural
@ -31,6 +36,13 @@ public class ImmediateTextNode: TextNode {
return layout.size
}
public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo {
let makeLayout = TextNode.asyncLayout(self)
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets))
let _ = apply()
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated)
}
override public func didLoad() {
super.didLoad()

View File

@ -97,11 +97,12 @@ public final class TextNodeLayout: NSObject {
fileprivate let cutout: TextNodeCutout?
fileprivate let insets: UIEdgeInsets
public let size: CGSize
public let truncated: Bool
fileprivate let firstLineOffset: CGFloat
fileprivate let lines: [TextNodeLine]
public let hasRTL: Bool
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) {
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) {
self.attributedString = attributedString
self.maximumNumberOfLines = maximumNumberOfLines
self.truncationType = truncationType
@ -111,6 +112,7 @@ public final class TextNodeLayout: NSObject {
self.cutout = cutout
self.insets = insets
self.size = size
self.truncated = truncated
self.firstLineOffset = firstLineOffset
self.lines = lines
self.backgroundColor = backgroundColor
@ -327,7 +329,7 @@ public class TextNode: ASDisplayNode {
var maybeTypesetter: CTTypesetter?
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
if maybeTypesetter == nil {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
}
let typesetter = maybeTypesetter!
@ -364,6 +366,7 @@ public class TextNode: ASDisplayNode {
let firstLineOffset = floorToScreenPixels(fontDescent)
var truncated = false
var first = true
while true {
var lineConstrainedWidth = constrainedSize.width
@ -419,6 +422,7 @@ public class TextNode: ASDisplayNode {
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken
truncated = true
}
let lineWidth = min(constrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))))
@ -485,9 +489,9 @@ public class TextNode: ASDisplayNode {
}
}
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor)
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor)
} else {
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
}
}